From 43ba8bd8b02acd76cbf1c806fc08a4de02a2c74b Mon Sep 17 00:00:00 2001 From: Manquer Date: Thu, 26 Nov 2015 20:25:21 +0530 Subject: [PATCH 001/821] Spelling Fix in rest_api_for_humans.rst Changed wehter to Whether --- docs/rest_api_for_humans.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index 4240a1824..20463b0c5 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -3,7 +3,7 @@ REST API for Humans I have been introducing Eve at several conferences and meetups. A few people suggested that I post the slides on the Eve website, so here it is: a quick rundown on Eve features, along with a few code snippets and examples. Hopefully -it will do a good job in letting you decide wether Eve is valid solution for +it will do a good job in letting you decide whether Eve is valid solution for your use case. .. embedly:: http://speakerdeck.com/nicola/eve-rest-api-for-humans From f320263facac22a72ee6caa90dc2dfc4053d3fc6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 26 Nov 2015 18:28:03 +0100 Subject: [PATCH 002/821] Manquer Conflicts: AUTHORS --- AUTHORS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index 2deadb4f5..f15534848 100644 --- a/AUTHORS +++ b/AUTHORS @@ -71,6 +71,7 @@ Patches and Contributions - Kurt Doherty - Magdas Adrian - Mandar Vaze +- Manquer - Marc Abramowitz - Marcus Cobden - Marica Odagaki @@ -80,6 +81,7 @@ Patches and Contributions - Matthieu Prat - Mayur Dhamanwala - Mikael Berg +- Mugur Rus - Nathan Reynolds - Niall Donegan - Nick Park @@ -114,4 +116,5 @@ Patches and Contributions - Xavi Cubillas - boosh - dccrazyboy +- mmizotin - xgdgsc From 163997047b7e769582b536103fcaf3579004e15c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 26 Nov 2015 18:29:04 +0100 Subject: [PATCH 003/821] Changelog for #771 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index b0c3941bd..7a6cd354e 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Version 0.6.2 - Update: PyMongo 3.1 is now required. - Update: Flask-PyMongo 0.4+ is now required. +- Docs: fix some typos (Manquer). + Stable ------ From b9d124d28c092407bafe0f4aedfaf3aa20dd1d75 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Fri, 27 Nov 2015 17:31:41 +0100 Subject: [PATCH 004/821] Skip any null value from attempting serialization (Fixes #772) --- eve/methods/common.py | 4 +++- eve/tests/methods/common.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 2a6fbfab9..44b5ef550 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -328,6 +328,8 @@ def serialize(document, resource=None, schema=None, fields=None): if not fields: fields = document.keys() for field in fields: + if document[field] is None: + continue if field in schema: field_schema = schema[field] field_type = field_schema.get('type') @@ -341,7 +343,7 @@ def serialize(document, resource=None, schema=None, fields=None): if type(subdocument) is not dict: # value is not a dict - continue serialization # error will be reported by validation if - # appropriate (could be allowed nullable dict) + # appropriate continue elif 'schema' in field_schema: serialize(subdocument, diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 8c5385274..c0abc5447 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -168,6 +168,25 @@ def test_serialize_null_dictionary(self): self.assertTrue(False, "Serializing null dictionaries should " "not raise an exception.") + def test_serialize_null_list(self): + schema = { + 'nullable_list': { + 'type': 'list', + 'nullable': True, + 'schema': { + 'type': 'objectid' + } + } + } + doc = { + 'nullable_list': None + } + with self.app.app_context(): + try: + serialize(doc, schema=schema) + except Exception: + self.fail('Serializing null lists should not raise an exception') + class TestNormalizeDottedFields(TestBase): def test_normalize_dotted_fields(self): From 37ca7f4e0b0ac1f8f9419ba111e5efec55edd0f5 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Fri, 27 Nov 2015 20:04:20 +0100 Subject: [PATCH 005/821] Fix Flake8 --- eve/tests/methods/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index c0abc5447..1b5c9df6a 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -185,7 +185,8 @@ def test_serialize_null_list(self): try: serialize(doc, schema=schema) except Exception: - self.fail('Serializing null lists should not raise an exception') + self.fail('Serializing null lists' + ' should not raise an exception') class TestNormalizeDottedFields(TestBase): From c8b74e5d88f425d28dce6e9aa5d3b5a3c2c75ee4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 29 Nov 2015 08:44:35 +0100 Subject: [PATCH 006/821] Changelog update for #773 Conflicts: CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 7a6cd354e..e90e4a9c3 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,7 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: Skip any null value from serialization (Arnau Orriols). - Fix: When ``SOFT_DELETE`` is active an exclusive ``datasource.projection`` causes a ``500`` error. Closes #752. From 23c14e6704318fce96ef37695f7782a902a34e45 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Fri, 27 Nov 2015 19:49:56 +0100 Subject: [PATCH 007/821] Add missing serializer for fields of type number. The serializers of int, float and number are required when using Eve through x-www-urlencode requests. Added some test cases that covers this dependency. --- eve/io/mongo/mongo.py | 1 + eve/tests/methods/common.py | 14 ++++++++++++++ eve/tests/methods/patch.py | 11 +++++++++++ eve/tests/methods/post.py | 11 +++++++++++ eve/tests/methods/put.py | 11 +++++++++++ eve/tests/test_settings.py | 3 +++ 6 files changed, 51 insertions(+) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 4c58cae87..905f88459 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -66,6 +66,7 @@ class Mongo(DataLayer): 'datetime': str_to_date, 'integer': lambda value: int(value) if value is not None else None, 'float': lambda value: float(value) if value is not None else None, + 'number': lambda val: json.loads(val) if val is not None else None } # JSON serializer is a class attribute. Allows extensions to replace it diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 1b5c9df6a..b3c0ef116 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -188,6 +188,20 @@ def test_serialize_null_list(self): self.fail('Serializing null lists' ' should not raise an exception') + def test_serialize_number(self): + schema = { + 'anumber': { + 'type': 'number', + } + } + for expected_type, value in [(int, '35'), (float, '3.5')]: + doc = { + 'anumber': value + } + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertIsInstance(serialized['anumber'], expected_type) + class TestNormalizeDottedFields(TestBase): def test_normalize_dotted_fields(self): diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 89bda1074..aee65987e 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -270,6 +270,17 @@ def test_patch_x_www_form_urlencoded(self): self.assert200(status) self.assertTrue('OK' in r[STATUS]) + def test_patch_x_www_form_urlencoded_number_serialization(self): + del(self.domain['contacts']['schema']['ref']['required']) + field = 'anumber' + test_value = 3.5 + changes = {field: test_value} + headers = [('If-Match', self.item_etag)] + r, status = self.parse_response(self.test_client.patch( + self.item_id_url, data=changes, headers=headers)) + self.assert200(status) + self.assertTrue('OK' in r[STATUS]) + def test_patch_referential_integrity(self): data = {"person": self.unknown_item_id} headers = [('If-Match', self.invoice_etag)] diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index a51e9a835..1f19401d1 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -217,6 +217,17 @@ def test_post_x_www_form_urlencoded(self): self.assertTrue('OK' in r[STATUS]) self.assertPostResponse(r) + def test_post_x_www_form_urlencoded_number_serialization(self): + del(self.domain['contacts']['schema']['ref']['required']) + test_field = "anumber" + test_value = 34 + data = {test_field: test_value} + r, status = self.parse_response(self.test_client.post( + self.known_resource_url, data=data)) + self.assert201(status) + self.assertIn('OK', r[STATUS]) + self.assertPostResponse(r) + def test_post_referential_integrity(self): data = {"person": self.unknown_item_id} r, status = self.post('/invoices/', data=data) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index 0f7f4d1c8..cf06d8fc6 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -71,6 +71,17 @@ def test_put_x_www_form_urlencoded(self): self.assert200(status) self.assertTrue('OK' in r[STATUS]) + def test_put_x_www_form_urlencoded_number_serialization(self): + del(self.domain['contacts']['schema']['ref']['required']) + field = 'anumber' + test_value = 41 + changes = {field: test_value} + headers = [('If-Match', self.item_etag)] + r, status = self.parse_response(self.test_client.put( + self.item_id_url, data=changes, headers=headers)) + self.assert200(status) + self.assertTrue('OK' in r[STATUS]) + def test_put_referential_integrity(self): data = {"person": self.unknown_item_id} headers = [('If-Match', self.invoice_etag)] diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 9b21beb12..66136a773 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -143,6 +143,9 @@ }, 'afloat': { 'type': 'float', + }, + 'anumber': { + 'type': 'number' } } } From f3a839b62a80df6f2c14dcd6a7946e65edb74660 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Fri, 27 Nov 2015 20:21:00 +0100 Subject: [PATCH 008/821] Fix support for Python2.6 --- eve/tests/methods/common.py | 4 +++- eve/tests/methods/post.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index b3c0ef116..28d1a6813 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -200,7 +200,9 @@ def test_serialize_number(self): } with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertIsInstance(serialized['anumber'], expected_type) + self.assertTrue( + isinstance(serialized['anumber'], expected_type) + ) class TestNormalizeDottedFields(TestBase): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 1f19401d1..94e92fcec 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -225,7 +225,7 @@ def test_post_x_www_form_urlencoded_number_serialization(self): r, status = self.parse_response(self.test_client.post( self.known_resource_url, data=data)) self.assert201(status) - self.assertIn('OK', r[STATUS]) + self.assertTrue('OK' in r[STATUS]) self.assertPostResponse(r) def test_post_referential_integrity(self): From 7fe198857e48b599601dec622cdbffc2db416cc5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 1 Jan 2016 19:06:43 +0100 Subject: [PATCH 009/821] Changelog for #774 Conflicts: CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index e90e4a9c3..1611c02cd 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,7 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: Add missing serializer for fields of type ``number``(Arnau Orriols). - Fix: Skip any null value from serialization (Arnau Orriols). - Fix: When ``SOFT_DELETE`` is active an exclusive ``datasource.projection`` causes a ``500`` error. Closes #752. From 759ba544bf0aca16652a867a1a67dd0e06fdc815 Mon Sep 17 00:00:00 2001 From: Stratos Gerakakis Date: Tue, 24 Nov 2015 11:56:51 +0100 Subject: [PATCH 010/821] Validator is not set when skip_validation is true --- eve/methods/patch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 09ecd1e52..4e62f5114 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -135,8 +135,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] - if not skip_validation: - validator = app.validator(schema, resource) + validator = app.validator(schema, resource) object_id = original[resource_def['id_field']] last_modified = None From b98fed0b0be4f5ea0b94c416170b6b088dc64c4e Mon Sep 17 00:00:00 2001 From: Stratos Gerakakis Date: Sat, 28 Nov 2015 21:42:08 +0100 Subject: [PATCH 011/821] typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c16d5158d..732168910 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ allows to effortlessly build and deploy highly customizable, fully featured RESTful Web Services. Eve is powered by Flask, Redis, Cerberus, Events and offers support for both -MongoDB and SQL backends. +MongoDB and SQL backends The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4 and PyPy. From a003359b9b1b4e6a4a4684cde85a5c49fe43f0da Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Dec 2015 07:37:27 +0100 Subject: [PATCH 012/821] Improve resilience --- eve/methods/patch.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 4e62f5114..b885b388b 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -63,6 +63,9 @@ def patch_internal(resource, payload=None, concurrency_check=False, :param skip_validation: skip payload validation before write (bool) :param **lookup: document lookup query. + .. versionchanged:: 0.6.2 + Fix: validator is not set when skip_validation is true. + .. versionchanged:: 0.6 on_updated returns the updated document (#682). Allow restoring soft deleted documents via PATCH @@ -157,9 +160,10 @@ def patch_internal(resource, payload=None, concurrency_check=False, else: validation = validator.validate_update(updates, object_id, original) + updates = validator.document + if validation: # Apply coerced values - updates = validator.document # sneak in a shadow copy if it wasn't already there late_versioning_catch(original, resource) From 7b76fb61eaf8b16987df376d5ef803fb7ba85697 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Dec 2015 07:37:41 +0100 Subject: [PATCH 013/821] Changelog for #768 Conflicts: CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 1611c02cd..7c5b1e481 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: In ``patch_internal`` Validator is not se twhen ``skip_validation`` is + ``true`` (Stratos Gerakakis). - Fix: Add missing serializer for fields of type ``number``(Arnau Orriols). - Fix: Skip any null value from serialization (Arnau Orriols). - Fix: When ``SOFT_DELETE`` is active an exclusive ``datasource.projection`` From b1f29c6245d047eb65db8e686e7abeb980d90a23 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Dec 2015 07:40:15 +0100 Subject: [PATCH 014/821] Fix typo ingroduced with #768 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 732168910..c16d5158d 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ allows to effortlessly build and deploy highly customizable, fully featured RESTful Web Services. Eve is powered by Flask, Redis, Cerberus, Events and offers support for both -MongoDB and SQL backends +MongoDB and SQL backends. The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4 and PyPy. From cf8824e0701976e4b3c523618c8996808891d8e3 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Fri, 4 Dec 2015 23:49:43 +0100 Subject: [PATCH 015/821] Serialize inside *of and *of_type rules new in Cerberus0.9 (closes #692) --- eve/methods/common.py | 11 +++++++- eve/tests/methods/common.py | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 44b5ef550..83a5cc62b 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -333,6 +333,15 @@ def serialize(document, resource=None, schema=None, fields=None): if field in schema: field_schema = schema[field] field_type = field_schema.get('type') + if field_type is None: + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + schema = {field: optschema} + serialize(document, schema=schema) + x_of_type = '{}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + schema = {field: {'type': opttype}} + serialize(document, schema=schema) if 'schema' in field_schema: field_schema = field_schema['schema'] if 'dict' in (field_type, field_schema.get('type')): @@ -391,7 +400,7 @@ def serialize(document, resource=None, schema=None, fields=None): try: document[field] = \ app.data.serializers[field_type](document[field]) - except (ValueError, InvalidId): + except (ValueError, TypeError, InvalidId): # value can't be casted, we continue processing the # rest of the document. Validation will later report # back the issue. diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 28d1a6813..8348b2161 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -204,6 +204,59 @@ def test_serialize_number(self): isinstance(serialized['anumber'], expected_type) ) + def test_serialize_inside_x_of_rules(self): + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + schema = { + 'x_of-field': { + x_of: [ + {'type': 'objectid'}, + {'required': True} + ] + } + } + doc = {'x_of-field': '50656e4538345b39dd0414f0'} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + + def test_serialize_inside_nested_x_of_rules(self): + schema = { + 'nested-x_of-field': { + 'oneof': [ + { + 'anyof': [ + {'type': 'objectid'}, + {'type': 'datetime'} + ], + 'required': True + }, + { + 'allof': [ + {'type': 'boolean'}, + {'required': True} + ] + } + ] + } + } + doc = {'nested-x_of-field': '50656e4538345b39dd0414f0'} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertTrue( + isinstance(serialized['nested-x_of-field'], ObjectId)) + + def test_serialize_inside_x_of_typesavers(self): + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + schema = { + 'x_of-field': { + '{}_type'.format(x_of): ['objectid', 'float', 'boolean'] + } + } + doc = {'x_of-field': '50656e4538345b39dd0414f0'} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + class TestNormalizeDottedFields(TestBase): def test_normalize_dotted_fields(self): From 9e1034fa488c8a6d23b0290a0c4e0863e85abcd1 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Sat, 5 Dec 2015 00:23:26 +0100 Subject: [PATCH 016/821] Fix support for Python2.6 --- eve/methods/common.py | 2 +- eve/tests/methods/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 83a5cc62b..6a4abc73a 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -338,7 +338,7 @@ def serialize(document, resource=None, schema=None, fields=None): for optschema in field_schema.get(x_of, []): schema = {field: optschema} serialize(document, schema=schema) - x_of_type = '{}_type'.format(x_of) + x_of_type = '{0}_type'.format(x_of) for opttype in field_schema.get(x_of_type, []): schema = {field: {'type': opttype}} serialize(document, schema=schema) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 8348b2161..8a5f371a1 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -249,7 +249,7 @@ def test_serialize_inside_x_of_typesavers(self): for x_of in ['allof', 'anyof', 'oneof', 'noneof']: schema = { 'x_of-field': { - '{}_type'.format(x_of): ['objectid', 'float', 'boolean'] + '{0}_type'.format(x_of): ['objectid', 'float', 'boolean'] } } doc = {'x_of-field': '50656e4538345b39dd0414f0'} From c400adc1752147d37cb0257a7b57669d399d308b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 Dec 2015 07:39:32 +0100 Subject: [PATCH 017/821] Changelog for #778 Conflicts: CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 7c5b1e481..1901b603e 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: Serialize inside *of and *of_type rules new in Cerberus 0.9. Closes #692 + (Arnau Orriols). - Fix: In ``patch_internal`` Validator is not se twhen ``skip_validation`` is ``true`` (Stratos Gerakakis). - Fix: Add missing serializer for fields of type ``number``(Arnau Orriols). From ee7469935be8ee314e4f70cc0b872c094d94b6b2 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Wed, 16 Dec 2015 09:40:40 +0100 Subject: [PATCH 018/821] Fix typo in document embedding limitations example Same fix as https://github.com/nicolaiarocci/eve/pull/781 --- docs/features.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index a013e31f9..3158992d8 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -897,7 +897,7 @@ Limitations ~~~~~~~~~~~ Currently we support embedding of documents by references located in any subdocuments (nested dicts and lists). For example, a query -``/invoices?/embedded={"user.friends":1}`` will return a document with ``user`` +``/invoices/?embedded={"user.friends":1}`` will return a document with ``user`` and all his ``friends`` embedded, but only if ``user`` is a subdocument and ``friends`` is a list of reference (it could be a list of dicts, nested dict, etc.). This feature is about serialization on GET requests. There's no From 620aad611e73de5f494b4c78689cd34d847c80f2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 30 Dec 2015 07:50:33 +0100 Subject: [PATCH 019/821] Patrick Decat --- AUTHORS | 1 + CHANGES | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index f15534848..071b86ab3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -91,6 +91,7 @@ Patches and Contributions - Olivier Poitrey - Ondrej Slinták - Or Neeman +- Patrick Decat - Pau Freixes - Paul Doucet - Peter Darrow diff --git a/CHANGES b/CHANGES index 1901b603e..2ead6fab9 100644 --- a/CHANGES +++ b/CHANGES @@ -21,7 +21,7 @@ Version 0.6.2 - Update: PyMongo 3.1 is now required. - Update: Flask-PyMongo 0.4+ is now required. -- Docs: fix some typos (Manquer). +- Docs: fix some typos (Manquer, Patrick Decat). Stable From e47ffa6c76c9f9797eb1ab1f862f5d0dfb01bd2e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 30 Dec 2015 07:54:01 +0100 Subject: [PATCH 020/821] small typo Conflicts: CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 2ead6fab9..ad88a4e62 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,7 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ -- Fix: Serialize inside *of and *of_type rules new in Cerberus 0.9. Closes #692 +- Fix: Serialize inside ``of`` and ``of_type`` rules new in Cerberus 0.9. Closes #692 (Arnau Orriols). - Fix: In ``patch_internal`` Validator is not se twhen ``skip_validation`` is ``true`` (Stratos Gerakakis). From 6ccfa4efb9882d3f3cc71136e4f7c7534dac4147 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 9 Jan 2016 09:34:11 +0100 Subject: [PATCH 021/821] Fix crash when both SOFT_DELETE and ALLOW_UNKNOWN are True. Closes #800. --- CHANGES | 6 ++++-- eve/flaskapp.py | 7 ++++++- eve/tests/config.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index ad88a4e62..816ac0c1d 100644 --- a/CHANGES +++ b/CHANGES @@ -9,8 +9,10 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ -- Fix: Serialize inside ``of`` and ``of_type`` rules new in Cerberus 0.9. Closes #692 - (Arnau Orriols). +- Fix: startup crash when both ``SOFT_DELETE`` and ``ALLOW_UNKNOWN`` are + enabled. Closes #800. +- Fix: Serialize inside ``of`` and ``of_type`` rules new in Cerberus 0.9. + Closes #692 (Arnau Orriols). - Fix: In ``patch_internal`` Validator is not se twhen ``skip_validation`` is ``true`` (Stratos Gerakakis). - Fix: Add missing serializer for fields of type ``number``(Arnau Orriols). diff --git a/eve/flaskapp.py b/eve/flaskapp.py index af897bbcc..5a947cc49 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -501,6 +501,10 @@ def set_defaults(self): def _set_resource_defaults(self, resource, settings): """ Low-level method which sets default values for one resource. + .. versionchanged:: 0.6.2 + Fix: startup crash when both SOFT_DELETE and ALLOW_UNKNOWN are True. + + (#722). .. versionchanged:: 0.6.1 Fix: inclusive projection defined for a datasource is ignored (#722). @@ -623,7 +627,8 @@ def _set_resource_defaults(self, resource, settings): projection = None ds.setdefault('projection', projection) - if settings['soft_delete'] is True and not exclusion: + if settings['soft_delete'] is True and not exclusion and \ + ds['projection'] is not None: ds['projection'][self.config['DELETED']] = 1 # 'defaults' helper set contains the names of fields with default diff --git a/eve/tests/config.py b/eve/tests/config.py index 0f5c6cc46..e2c0f92ee 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -11,6 +11,18 @@ class TestConfig(TestBase): + def test_allow_unknown_with_soft_delete(self): + my_settings = { + 'ALLOW_UNKNOWN': True, + 'SOFT_DELETE': True, + 'DOMAIN': {'contacts': {}} + } + try: + self.app = Eve(settings=my_settings) + except TypeError: + self.fail("ALLOW_UNKNOWN and SOFT_DELETE enabled should not cause " + "a crash.") + def test_default_import_name(self): self.assertEqual(self.app.import_name, eve.__package__) From f533aa0305312ff3e84bf18b0e6013549713a7ff Mon Sep 17 00:00:00 2001 From: Ralph Smith Date: Thu, 17 Dec 2015 18:14:58 -0700 Subject: [PATCH 022/821] Fix #786 update ITEM_URL to match defualt_settings --- AUTHORS | 1 + eve/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 071b86ab3..04d874737 100644 --- a/AUTHORS +++ b/AUTHORS @@ -96,6 +96,7 @@ Patches and Contributions - Paul Doucet - Peter Darrow - Petr Jašek +- Ralph Smith - Robert Wlodarczyk - Roberto 'Kalamun' Pasini - Ronan Delacroix diff --git a/eve/__init__.py b/eve/__init__.py index 1a98cc90d..143f10ee1 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -57,7 +57,7 @@ ITEM_METHODS = ['GET'] ITEM_LOOKUP = True ITEM_LOOKUP_FIELD = ID_FIELD -ITEM_URL = '[a-f0-9]{24}' +ITEM_URL = 'regex("[a-f0-9]{24}")' STATUS_OK = "OK" STATUS_ERR = "ERR" From dd3e444a9071f1129bf002eecc8aa84b6efb9199 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 9 Jan 2016 09:46:17 +0100 Subject: [PATCH 023/821] Changelog update for #787 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 816ac0c1d..b945fa73a 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: the ``__init__.py`` ``ITEM_URL`` does not match default_settings.py. + Closes #786 (Ralph Smith). - Fix: startup crash when both ``SOFT_DELETE`` and ``ALLOW_UNKNOWN`` are enabled. Closes #800. - Fix: Serialize inside ``of`` and ``of_type`` rules new in Cerberus 0.9. From 6554339a8749173c2d3ccaa531b31a411ec97068 Mon Sep 17 00:00:00 2001 From: nckpark Date: Tue, 12 Jan 2016 21:28:42 -0800 Subject: [PATCH 024/821] Standardize value serialization exception handling across the serialize function. --- eve/methods/common.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 6a4abc73a..3099c2528 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -369,15 +369,14 @@ def serialize(document, resource=None, schema=None, fields=None): serialize(sublist[i], schema=sublist_schema['schema']) elif item_type in app.data.serializers: - sublist[i] = \ - app.data.serializers[item_type](v) + sublist[i] = serialize_value(item_type, v) else: # a list of one type, arbitrary length field_type = field_schema.get('type') if field_type in app.data.serializers: for i, v in enumerate(document[field]): document[field][i] = \ - app.data.serializers[field_type](v) + serialize_value(field_type, v) elif 'items' in field_schema: # a list of multiple types, fixed length for i, (s, v) in enumerate(zip(field_schema['items'], @@ -385,8 +384,7 @@ def serialize(document, resource=None, schema=None, fields=None): field_type = s.get('type') if field_type in app.data.serializers: document[field][i] = \ - app.data.serializers[field_type]( - document[field][i]) + serialize_value(field_type, document[field][i]) elif 'valueschema' in field_schema: # a valueschema field_type = field_schema['valueschema']['type'] @@ -394,20 +392,27 @@ def serialize(document, resource=None, schema=None, fields=None): target = document[field] for field in target: target[field] = \ - app.data.serializers[field_type](target[field]) + serialize_value(field_type, target[field]) elif field_type in app.data.serializers: # a simple field - try: - document[field] = \ - app.data.serializers[field_type](document[field]) - except (ValueError, TypeError, InvalidId): - # value can't be casted, we continue processing the - # rest of the document. Validation will later report - # back the issue. - pass + document[field] = \ + serialize_value(field_type, document[field]) + return document +def serialize_value(field_type, value): + """Serialize value of a given type. Relies on the app.data.serializers + dictionary. + """ + try: + return app.data.serializers[field_type](value) + except (KeyError, ValueError, TypeError, InvalidId): + # value can't be cast or no serializer defined, return as is and + # validation will later report back the issue. + return value + + def normalize_dotted_fields(document): """ Normalizes eventual dotted fields so validation can be performed seamlessly. For example this document: From 95af6bbae5e56e3a73ca5c8d1e3c1a264d1c4955 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 16 Jan 2016 08:16:25 +0100 Subject: [PATCH 025/821] Changelog update for #804. --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index b945fa73a..109786fc6 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: do not attempt to parse ``number`` values as strings when they are + numerical (Nick Park). - Fix: the ``__init__.py`` ``ITEM_URL`` does not match default_settings.py. Closes #786 (Ralph Smith). - Fix: startup crash when both ``SOFT_DELETE`` and ``ALLOW_UNKNOWN`` are From a9679d729652ac3cb167dd50fb21c81d61fb0aeb Mon Sep 17 00:00:00 2001 From: "Valerie R. Coffman" Date: Mon, 11 Jan 2016 12:31:26 -0500 Subject: [PATCH 026/821] allow the options method on the schema endpoint --- eve/flaskapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 5a947cc49..9a306c56b 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -919,11 +919,11 @@ def _init_schema_endpoint(self): # add schema collections url self.add_url_rule(schema_url, 'schema_collection', view_func=schema_collection_endpoint, - methods=['GET']) + methods=['GET', 'OPTIONS']) # add schema item url self.add_url_rule(schema_url + '/', 'schema_item', view_func=schema_item_endpoint, - methods=['GET']) + methods=['GET', 'OPTIONS']) def __call__(self, environ, start_response): """ If HTTP_X_METHOD_OVERRIDE is included with the request and method From ceb867073b6706a61f0b788488de2a33cb5c65ae Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 16:53:11 +0100 Subject: [PATCH 027/821] Add test for PR #801 --- eve/tests/renders.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index cbaf9fcf0..2716d8503 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -280,4 +280,10 @@ def test_CORS_OPTIONS_item(self): self.test_CORS_OPTIONS(url, methods) url = '%s%s/%s' % (prefix, self.known_resource_url, self.item_ref) methods = ['GET', 'OPTIONS'] - self.test_CORS_OPTIONS(url, methods) + + def test_CORS_OPTIONS_schema(self): + """ Test that CORS is also supported at SCHEMA_ENDPOINT """ + self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app._init_schema_endpoint() + methods = ['GET', 'OPTIONS'] + self.test_CORS_OPTIONS('schema', methods) From 01cbb90fc213482f5255aa116c1a0a77463eb527 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 16:59:09 +0100 Subject: [PATCH 028/821] Valerie Coffman --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 04d874737..f5023a34d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -114,6 +114,7 @@ Patches and Contributions - Thomas Sileo - Tim Jacobi - Tomasz Jezierski +- Valerie Coffman - Wael M. Nasreddine - Xavi Cubillas - boosh From 38cb90572c36e1e540668eca7880ccc20ca6fb2b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 16:59:19 +0100 Subject: [PATCH 029/821] Changelog for #801 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 109786fc6..413225602 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: CORS pre-flight requests malfunction on SCHEMA_ENDPOINT endpoint + (Valerie Coffman). - Fix: do not attempt to parse ``number`` values as strings when they are numerical (Nick Park). - Fix: the ``__init__.py`` ``ITEM_URL`` does not match default_settings.py. From 25c41590293192a16de17fd514efa818c42cf474 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 17:36:45 +0100 Subject: [PATCH 030/821] Fix SCHEMA_ENDPOINT crash when lambda used w/'coerce' rule Closes #790. Conflicts: CHANGES --- CHANGES | 6 ++++++ eve/io/mongo/mongo.py | 14 +++++++++++--- eve/tests/endpoints.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 413225602..59a6709d8 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,13 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +<<<<<<< HEAD - Fix: CORS pre-flight requests malfunction on SCHEMA_ENDPOINT endpoint +======= +- Fix: ``SCHEMA_ENDPOINT`` does not work when schema has lambda function as + ``coerce`` rule. Closes #790. +- Fix: CORS pre-flight requests malfunction on ``SCHEMA_ENDPOINT`` endpoint +>>>>>>> e2e339a... Fix SCHEMA_ENDPOINT crash when lambda used w/'coerce' rule (Valerie Coffman). - Fix: do not attempt to parse ``number`` values as strings when they are numerical (Nick Park). diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 905f88459..ba96a3f9d 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -33,15 +33,23 @@ class MongoJSONEncoder(BaseJSONEncoder): """ Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. + .. versionchanged:: 0.6.2 + Do not attempt to serialize callables. Closes #790. + .. versionadded:: 0.2 """ def default(self, obj): if isinstance(obj, ObjectId): # BSON/Mongo ObjectId is rendered as a string return str(obj) - else: - # delegate rendering to base class method - return super(MongoJSONEncoder, self).default(obj) + if callable(obj): + # when SCHEMA_ENDPOINT is active, 'coerce' rule is likely to + # contain a lambda/callable which can't be jSON serialized + # (and we probably don't want it to be exposed anyway). See #790. + return "" + + # delegate rendering to base class method + return super(MongoJSONEncoder, self).default(obj) class Mongo(DataLayer): diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 66333b5c1..f6dcb32bc 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -336,3 +336,17 @@ def test_schema_endpoint(self): self.assert405(status_code) _, status_code = self.delete(known_schema_path) self.assert405(status_code) + + def test_schema_endpoint_does_not_attempt_callable_serialization(self): + self.domain[self.known_resource]['schema']['lambda'] = { + 'type': 'boolean', + 'coerce': lambda v: v if type(v) is bool else v.lower() in ['true', + '1'] + } + known_schema_path = '/schema/%s' % self.known_resource + self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app._init_schema_endpoint() + + r = self.test_client.get(known_schema_path) + self.assert200(r.status_code) + self.assertEqual(json.loads(r.data)['lambda']['coerce'], '') From 72ccfd4a6adc67776b3fc9bc4b9f09e7dbcaf6be Mon Sep 17 00:00:00 2001 From: Prayag Verma Date: Sat, 16 Jan 2016 18:21:37 +0530 Subject: [PATCH 031/821] Update license year to 2016 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index aac3f42e3..ac19726ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 by Nicola Iarocci and contributors. See AUTHORS +Copyright (c) 2016 by Nicola Iarocci and contributors. See AUTHORS for more details. Some rights reserved. From c76204b23a56f4c4db40d14e328d7e8778570d05 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 18:02:40 +0100 Subject: [PATCH 032/821] Prayag Verma --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index f5023a34d..85854d814 100644 --- a/AUTHORS +++ b/AUTHORS @@ -96,6 +96,7 @@ Patches and Contributions - Paul Doucet - Peter Darrow - Petr Jašek +- Prayag Verma - Ralph Smith - Robert Wlodarczyk - Roberto 'Kalamun' Pasini From 4d860064a4cc352feb4755485dc2b321e39237a0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Jan 2016 18:03:56 +0100 Subject: [PATCH 033/821] Update license to 2016 for remaining files Conflicts: CHANGES --- CHANGES | 4 +--- eve/__init__.py | 2 +- eve/auth.py | 2 +- eve/default_settings.py | 2 +- eve/defaults.py | 2 +- eve/endpoints.py | 2 +- eve/exceptions.py | 2 +- eve/flaskapp.py | 2 +- eve/io/__init__.py | 2 +- eve/io/base.py | 2 +- eve/io/media.py | 2 +- eve/io/mongo/__init__.py | 2 +- eve/io/mongo/geo.py | 2 +- eve/io/mongo/media.py | 2 +- eve/io/mongo/mongo.py | 2 +- eve/io/mongo/parser.py | 2 +- eve/io/mongo/validation.py | 2 +- eve/methods/__init__.py | 2 +- eve/methods/common.py | 2 +- eve/methods/delete.py | 2 +- eve/methods/get.py | 2 +- eve/methods/patch.py | 2 +- eve/methods/post.py | 2 +- eve/methods/put.py | 2 +- eve/render.py | 2 +- eve/utils.py | 2 +- eve/validation.py | 2 +- 27 files changed, 27 insertions(+), 29 deletions(-) diff --git a/CHANGES b/CHANGES index 59a6709d8..905524164 100644 --- a/CHANGES +++ b/CHANGES @@ -9,13 +9,10 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ -<<<<<<< HEAD - Fix: CORS pre-flight requests malfunction on SCHEMA_ENDPOINT endpoint -======= - Fix: ``SCHEMA_ENDPOINT`` does not work when schema has lambda function as ``coerce`` rule. Closes #790. - Fix: CORS pre-flight requests malfunction on ``SCHEMA_ENDPOINT`` endpoint ->>>>>>> e2e339a... Fix SCHEMA_ENDPOINT crash when lambda used w/'coerce' rule (Valerie Coffman). - Fix: do not attempt to parse ``number`` values as strings when they are numerical (Nick Park). @@ -36,6 +33,7 @@ Version 0.6.2 - Update: Flask-PyMongo 0.4+ is now required. - Docs: fix some typos (Manquer, Patrick Decat). +- Update license to 2016 (Prayag Verma) Stable diff --git a/eve/__init__.py b/eve/__init__.py index 143f10ee1..bbb5a339e 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -6,7 +6,7 @@ An out-of-the-box REST Web API that's as dangerous as you want it to be. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. .. versionchanged:: 0.5 diff --git a/eve/auth.py b/eve/auth.py index 75eab565f..a11037f80 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -6,7 +6,7 @@ Allow API endpoints to be secured via BasicAuth and derivates. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from flask import request, Response, current_app as app, g, abort diff --git a/eve/default_settings.py b/eve/default_settings.py index 3cde6747c..2eb9fcff3 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -8,7 +8,7 @@ appropriately, by using a custom settings module (see the optional 'settings' argument or the EVE_SETTING environment variable). - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. .. versionchanged:: 0.6 diff --git a/eve/defaults.py b/eve/defaults.py index 86d1eca17..1d59bec3a 100644 --- a/eve/defaults.py +++ b/eve/defaults.py @@ -10,7 +10,7 @@ checked for a missing value, and if a value is missing the default is added. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/endpoints.py b/eve/endpoints.py index b4bd962b6..580671f0b 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -8,7 +8,7 @@ home) invokes the appropriate method handler, returning its response to the client, properly rendered. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from bson import tz_util diff --git a/eve/exceptions.py b/eve/exceptions.py index deee7bba5..b5c8c3b78 100644 --- a/eve/exceptions.py +++ b/eve/exceptions.py @@ -6,7 +6,7 @@ This module implements Eve custom exceptions. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 9a306c56b..8e183687c 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -6,7 +6,7 @@ This module implements the central WSGI application object as a Flask subclass. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import os diff --git a/eve/io/__init__.py b/eve/io/__init__.py index 622385822..04930df52 100644 --- a/eve/io/__init__.py +++ b/eve/io/__init__.py @@ -6,7 +6,7 @@ This package implements the data layers supported by Eve. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/base.py b/eve/io/base.py index ab7ea545f..8ff14af21 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -6,7 +6,7 @@ Standard interface implemented by Eve data layers. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import datetime diff --git a/eve/io/media.py b/eve/io/media.py index 3c4fc6849..135db9d0d 100644 --- a/eve/io/media.py +++ b/eve/io/media.py @@ -6,7 +6,7 @@ Media storage for Eve-powered APIs. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/__init__.py b/eve/io/mongo/__init__.py index 39a6cb51b..5e22ddc11 100644 --- a/eve/io/mongo/__init__.py +++ b/eve/io/mongo/__init__.py @@ -6,7 +6,7 @@ This package implements the MongoDB data layer. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index 03e324719..53bb1fa16 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -6,7 +6,7 @@ Geospatial functions and classes for mongo IO layer - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 5aed38548..f4bf61208 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -4,7 +4,7 @@ GridFS media storage for Eve-powered APIs. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from bson import ObjectId diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index ba96a3f9d..f075818bd 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -6,7 +6,7 @@ The actual implementation of the MongoDB data layer. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import itertools diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index a4fce245f..7e357a583 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -7,7 +7,7 @@ This module implements a Python-to-Mongo syntax parser. Allows the MongoDB data-layer to seamlessy respond to a Python-like query. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 6218b46d0..87d6331e0 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -8,7 +8,7 @@ objects incoming via POST/PATCH requests conform to the API domain. An extension of Cerberus Validator. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import copy diff --git a/eve/methods/__init__.py b/eve/methods/__init__.py index 2e381aa68..ad7e3d935 100644 --- a/eve/methods/__init__.py +++ b/eve/methods/__init__.py @@ -6,7 +6,7 @@ This package implements the HTTP methods supported by Eve. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/common.py b/eve/methods/common.py index 3099c2528..523775eea 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -6,7 +6,7 @@ Utility functions for API methods implementations. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import time diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 35d562986..183ccf157 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -6,7 +6,7 @@ This module imlements the DELETE method. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/get.py b/eve/methods/get.py index 52dbad2ac..047f2fc7f 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -7,7 +7,7 @@ This module implements the API 'GET' methods, supported by both the resources and single item endpoints. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import math diff --git a/eve/methods/patch.py b/eve/methods/patch.py index b885b388b..9c20ff75b 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -6,7 +6,7 @@ This module imlements the PATCH method. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/post.py b/eve/methods/post.py index b26c5ef22..7e2503299 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -7,7 +7,7 @@ This module imlements the POST method, supported by the resources endopints. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/put.py b/eve/methods/put.py index 1de455d72..562dcac08 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -6,7 +6,7 @@ This module imlements the PUT method. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from datetime import datetime diff --git a/eve/render.py b/eve/render.py index 18c29833c..9f0203d77 100644 --- a/eve/render.py +++ b/eve/render.py @@ -6,7 +6,7 @@ Implements proper, automated rendering for Eve responses. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/utils.py b/eve/utils.py index 8e316317b..c5b092461 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -6,7 +6,7 @@ Utility functions and classes. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/validation.py b/eve/validation.py index a00a851fa..85a9b8f59 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -8,7 +8,7 @@ datalayer-agnostic. Specialized Validator classes are implemented in the datalayer submodules. - :copyright: (c) 2015 by Nicola Iarocci. + :copyright: (c) 2016 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ From 47d44c00d5c3fb24aa27c5fc5dea71657593a77e Mon Sep 17 00:00:00 2001 From: Wei Guan Date: Sun, 17 Jan 2016 16:28:49 -0500 Subject: [PATCH 034/821] fix PUT validator issues and add test when skip_validation is true --- eve/methods/put.py | 5 ++--- eve/tests/methods/put.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/eve/methods/put.py b/eve/methods/put.py index 562dcac08..ef0347eaa 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -109,8 +109,7 @@ def put_internal(resource, payload=None, concurrency_check=False, """ resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] - if not skip_validation: - validator = app.validator(schema, resource) + validator = app.validator(schema, resource) if payload is None: payload = payload_() @@ -149,10 +148,10 @@ def put_internal(resource, payload=None, concurrency_check=False, else: validation = validator.validate_replace(document, object_id, original) - if validation: # Apply coerced values document = validator.document + if validation: # sneak in a shadow copy if it wasn't already there late_versioning_catch(original, resource) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index cf06d8fc6..fe79cabbf 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -265,6 +265,19 @@ def test_put_internal(self): self.assertEqual(db_value, test_value) self.assert200(status) + def test_put_internal_skip_validation(self): + # test that put_internal is available and working properly. + test_field = 'ref' + test_value = "9876543210987654321098765" + data = {test_field: test_value} + with self.app.test_request_context(self.item_id_url): + r, _, _, status = put_internal( + self.known_resource, data, concurrency_check=False, + skip_validation=True, **{'_id': self.item_id}) + db_value = self.compare_put_with_get(test_field, r) + self.assertEqual(db_value, test_value) + self.assert200(status) + def test_put_etag_header(self): # test that Etag is always includer with response header. See #562. changes = {"ref": "1234567890123456789012345"} From dffc3f435a58d54939c2c31554483ab7aba7d70f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 21 Jan 2016 17:52:08 +0100 Subject: [PATCH 035/821] Changelog for PR #812 --- CHANGES | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 905524164..0017eb623 100644 --- a/CHANGES +++ b/CHANGES @@ -22,7 +22,9 @@ Version 0.6.2 enabled. Closes #800. - Fix: Serialize inside ``of`` and ``of_type`` rules new in Cerberus 0.9. Closes #692 (Arnau Orriols). -- Fix: In ``patch_internal`` Validator is not se twhen ``skip_validation`` is +- Fix: In ``put_internal`` Validator is not set when ``skip_validation`` is + ``true`` (Wei Guan). +- Fix: In ``patch_internal`` Validator is not set when ``skip_validation`` is ``true`` (Stratos Gerakakis). - Fix: Add missing serializer for fields of type ``number``(Arnau Orriols). - Fix: Skip any null value from serialization (Arnau Orriols). From 730b58c08922fbec0e50852b33d4407f7b4ce4fb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 21 Jan 2016 17:52:21 +0100 Subject: [PATCH 036/821] Wei Guan --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 85854d814..762fd98a7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -117,6 +117,7 @@ Patches and Contributions - Tomasz Jezierski - Valerie Coffman - Wael M. Nasreddine +- Wei Guan - Xavi Cubillas - boosh - dccrazyboy From e79177c25cb8398ed506145c106243cdc2fed3b7 Mon Sep 17 00:00:00 2001 From: Luca Di Gaspero Date: Thu, 17 Dec 2015 14:02:39 +0100 Subject: [PATCH 037/821] Fixed a bug in TokenAuth. Namely, when the token were passed as "Authorization: " or "Authorization: Token " headers, the werkzeug parse_authorization did not recognize it as a valid authorization header, therefore the request.authorization field was empty. The workaround now considers, in addition to the werkzeug parsed authorization, also a direct manipulation of the "Authorization" header. --- eve/auth.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index a11037f80..e420d49e0 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -241,8 +241,13 @@ def authorized(self, allowed_roles, resource, method): string or a list of roles. :param resource: resource being requested. """ - auth = request.authorization - return auth and self.check_auth(auth.username, allowed_roles, resource, + if request.authorization: + auth = request.authorization.username + else: + auth = request.headers.get('Authorization').strip() + if auth.startswith('Token') or auth.startswith('token'): + auth = auth.split(" ")[1] + return auth and self.check_auth(auth, allowed_roles, resource, method) From d37a388d2e4572efabe26fa15a4d2f72bbdf6472 Mon Sep 17 00:00:00 2001 From: Luca Di Gaspero Date: Thu, 17 Dec 2015 14:24:27 +0100 Subject: [PATCH 038/821] Minor bug (used a tab instead of spaces in previous commit). --- eve/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/auth.py b/eve/auth.py index e420d49e0..e3893224c 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -241,7 +241,7 @@ def authorized(self, allowed_roles, resource, method): string or a list of roles. :param resource: resource being requested. """ - if request.authorization: + if request.authorization: auth = request.authorization.username else: auth = request.headers.get('Authorization').strip() From 7be0cdcacca011b7a9bee6682ca01216afaa722c Mon Sep 17 00:00:00 2001 From: Luca Di Gaspero Date: Thu, 17 Dec 2015 15:24:59 +0100 Subject: [PATCH 039/821] This should be finally ok, sorry for the multiple commits. --- eve/auth.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index e3893224c..2986e1d5a 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -241,12 +241,12 @@ def authorized(self, allowed_roles, resource, method): string or a list of roles. :param resource: resource being requested. """ - if request.authorization: - auth = request.authorization.username - else: + auth = request.authorization.username if hasattr(request.authorization, 'username') else None + # Werkzeug parse_authorization does not handle "Authorization: " or "Authorization: Token " headers, therefore they should be explicitly handled + if not auth and request.headers.get('Authorization'): auth = request.headers.get('Authorization').strip() if auth.startswith('Token') or auth.startswith('token'): - auth = auth.split(" ")[1] + auth = auth.split(' ')[1] return auth and self.check_auth(auth, allowed_roles, resource, method) From b10ae9952f965eadb503f56ba80b4f6739ce9799 Mon Sep 17 00:00:00 2001 From: Luca Di Gaspero Date: Thu, 17 Dec 2015 15:57:46 +0100 Subject: [PATCH 040/821] Didn't consider flake8 compliance. Moreover, added a test for the use case with "Authorization: Token ". --- eve/auth.py | 9 +++++++-- eve/tests/auth.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index 2986e1d5a..095af21e1 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -241,8 +241,13 @@ def authorized(self, allowed_roles, resource, method): string or a list of roles. :param resource: resource being requested. """ - auth = request.authorization.username if hasattr(request.authorization, 'username') else None - # Werkzeug parse_authorization does not handle "Authorization: " or "Authorization: Token " headers, therefore they should be explicitly handled + auth = None + if hasattr(request.authorization, 'username'): + auth = request.authorization.username + # Werkzeug parse_authorization does not handle + # "Authorization: " or + # "Authorization: Token " + # headers, therefore they should be explicitly handled if not auth and request.headers.get('Authorization'): auth = request.headers.get('Authorization').strip() if auth.startswith('Token') or auth.startswith('token'): diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 4f0e7ae97..465b843db 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -30,6 +30,10 @@ def check_auth(self, token, allowed_roles, resource, method): allowed_roles else True) +class BadTokenAuth(TokenAuth): + pass + + class ValidHMACAuth(HMACAuth): def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, resource, method): @@ -273,6 +277,20 @@ def test_custom_auth(self): self.assertTrue(isinstance(self.app.auth, ValidTokenAuth)) +class TestCustomTokenAuth(TestTokenAuth): + def setUp(self): + super(TestCustomTokenAuth, self).setUp() + self.valid_auth = [('Authorization', 'Token test_token'), + self.content_type] + + def test_bad_auth_class(self): + self.app = Eve(settings=self.settings_file, auth=BadTokenAuth) + self.test_client = self.app.test_client() + r = self.test_client.get('/', headers=self.valid_auth) + # will fail because check_auth() is not implemented in the custom class + self.assert500(r.status_code) + + class TestHMACAuth(TestBasicAuth): def setUp(self): super(TestHMACAuth, self).setUp() From 115aeec6545f5daf6df61ff5a009ef8fab6b012d Mon Sep 17 00:00:00 2001 From: Luca Di Gaspero Date: Thu, 7 Jan 2016 16:20:25 +0100 Subject: [PATCH 041/821] Minor fix to token based authorization. --- eve/auth.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index 095af21e1..148ef5590 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -226,13 +226,10 @@ def check_auth(self, token, allowed_roles, resource, method): raise NotImplementedError def authenticate(self): - """ Returns a standard a 401 response that enables basic auth. - Override if you want to change the response and/or the realm. + """ Returns a standard a 401. Override if you want to change the + response. """ - resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' % - __package__}) - abort(401, description='Please provide proper credentials', - response=resp) + abort(401, description='Please provide proper credentials') def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. From 7d3a323f0d581861eef1d415fe2fd37af4a935fa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 31 Jan 2016 09:19:58 +0100 Subject: [PATCH 042/821] Restore RFC2617 compliant Response object --- eve/auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eve/auth.py b/eve/auth.py index 148ef5590..855435db3 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -229,7 +229,10 @@ def authenticate(self): """ Returns a standard a 401. Override if you want to change the response. """ - abort(401, description='Please provide proper credentials') + resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' % + __package__}) + abort(401, description='Please provide proper credentials', + response=resp) def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. From 3be912b7af9e87cf62e583995eed532bd6461289 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 31 Jan 2016 09:28:25 +0100 Subject: [PATCH 043/821] Also support lowercase 'token' --- eve/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index 855435db3..c54980543 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -244,14 +244,16 @@ def authorized(self, allowed_roles, resource, method): auth = None if hasattr(request.authorization, 'username'): auth = request.authorization.username + # Werkzeug parse_authorization does not handle # "Authorization: " or # "Authorization: Token " # headers, therefore they should be explicitly handled if not auth and request.headers.get('Authorization'): - auth = request.headers.get('Authorization').strip() - if auth.startswith('Token') or auth.startswith('token'): + auth = request.headers.get('Authorization').strip().lower() + if auth.startswith('token'): auth = auth.split(' ')[1] + return auth and self.check_auth(auth, allowed_roles, resource, method) From ff8aaeea37fc80bf678c23b2a0ce1bdb6f45655b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 31 Jan 2016 09:35:29 +0100 Subject: [PATCH 044/821] Changelog for #783 Conflicts: CHANGES --- CHANGES | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 0017eb623..ccc7fc83b 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,10 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ -- Fix: CORS pre-flight requests malfunction on SCHEMA_ENDPOINT endpoint +- Fix: TokenAuth. When the tokens are passed as "Authorization: " or + "Authorization: Token " headers, werkzeug does not recognize them as valid + authorization header, therefore the ``request.authorization`` field is empty + (Luca Di Gaspero). - Fix: ``SCHEMA_ENDPOINT`` does not work when schema has lambda function as ``coerce`` rule. Closes #790. - Fix: CORS pre-flight requests malfunction on ``SCHEMA_ENDPOINT`` endpoint From c6644b41dbb3e97ee1f2c149802dbeb4d331c0bb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 31 Jan 2016 09:35:37 +0100 Subject: [PATCH 045/821] Luca Di Gaspero --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 762fd98a7..90d375b20 100644 --- a/AUTHORS +++ b/AUTHORS @@ -69,6 +69,7 @@ Patches and Contributions - Kracekumar - Kurt Bonne - Kurt Doherty +- Luca Di Gaspero - Magdas Adrian - Mandar Vaze - Manquer From c839806ecead0481d80927471f59f5a34ad15cf9 Mon Sep 17 00:00:00 2001 From: cbonnard Date: Mon, 1 Feb 2016 11:54:05 +0100 Subject: [PATCH 046/821] Fix issue #814 Signed-off-by: cbonnard --- eve/methods/delete.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 183ccf157..bb926e18b 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -119,7 +119,10 @@ def deleteitem_internal( late_versioning_catch(original, resource) # and add deleted version insert_versioning_documents(resource, marked_document) - else: + # update oplog if needed + oplog_push(resource, marked_document, 'DELETE', id) + + else: # Delete the document for real # media cleanup @@ -152,8 +155,8 @@ def deleteitem_internal( {versioned_id_field(resource_def): original[resource_def['id_field']]}) - # update oplog if needed - oplog_push(resource, original, 'DELETE', id) + # update oplog if needed + oplog_push(resource, original, 'DELETE', id) if suppress_callbacks is not True: getattr(app, "on_deleted_item")(resource, original) From 0a8ecd6d729488f04d667487d90cb4f602cdf430 Mon Sep 17 00:00:00 2001 From: cbonnard Date: Mon, 1 Feb 2016 14:51:51 +0100 Subject: [PATCH 047/821] Fix #814 Issue, indentation matching Signed-off-by: cbonnard --- eve/methods/delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/delete.py b/eve/methods/delete.py index bb926e18b..268ea8f7e 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -122,7 +122,7 @@ def deleteitem_internal( # update oplog if needed oplog_push(resource, marked_document, 'DELETE', id) - else: + else: # Delete the document for real # media cleanup From 9582faf188cc80244c2e62ef048353d6a576472b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Feb 2016 08:50:23 +0100 Subject: [PATCH 048/821] Add test for oplog update on soft deletes --- eve/tests/methods/common.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 8a5f371a1..d95ac0d5b 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -1,4 +1,5 @@ from datetime import datetime +import time import simplejson as json from bson import ObjectId @@ -435,6 +436,24 @@ def test_delete_oplog(self): oplog_entry = r['_items'][0] self.assertOpLogEntry(oplog_entry, 'DELETE') + def test_soft_delete_oplog(self): + r, s = self.parse_response(self.test_client.get(self.item_id_url)) + doc_date = r[config.LAST_UPDATED] + time.sleep(1) + + self.domain[self.known_resource]['soft_delete'] = True + + self.headers.append(('If-Match', self.item_etag)) + r = self.test_client.delete(self.item_id_url, + headers=self.headers, + environ_base={'REMOTE_ADDR': '127.0.0.1'}) + r, status = self.oplog_get() + self.assert200(status) + self.assertEqual(len(r['_items']), 1) + oplog_entry = r['_items'][0] + self.assertOpLogEntry(oplog_entry, 'DELETE') + self.assertTrue(doc_date != oplog_entry[config.LAST_UPDATED]) + def patch(self, url, data, headers=[], content_type='application/json'): headers.append(('Content-Type', content_type)) headers.append(('If-Match', self.item_etag)) From 1430209955dc2869cd09df0da784f42b30dfe620 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Feb 2016 08:26:14 +0100 Subject: [PATCH 049/821] Changelog for #816 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index ccc7fc83b..e46610511 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,9 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: when a document is soft deleted, the OPLOG `_updated` field is not the + time of the deletion but the time of the previous last update (Cyril + Bonnard). - Fix: TokenAuth. When the tokens are passed as "Authorization: " or "Authorization: Token " headers, werkzeug does not recognize them as valid authorization header, therefore the ``request.authorization`` field is empty From fc9adee0dfb609f9b9eb9ea93e30d1c4b2477cfb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Feb 2016 08:26:29 +0100 Subject: [PATCH 050/821] Cyril Bonnard --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 90d375b20..f0d426b5d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,6 +22,7 @@ Patches and Contributions - Christoph Witzany - Christopher Larsen - Cyprien Pannier +- Cyril Bonnard - Daniel Lytkin - Daniele Pizzolli - Danse From 2f84283d7542a0d42469151cadd69b512b4650db Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 23 Feb 2016 11:16:35 +0100 Subject: [PATCH 051/821] Fix: 409 not reported since upgrading to PyMongo 3 Closes #680. --- CHANGES | 1 + eve/io/mongo/mongo.py | 36 +++++++++++++++++++++--------------- eve/tests/methods/post.py | 9 +++++++++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CHANGES b/CHANGES index e46610511..539eeabb2 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,7 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: ``409 Conflict`` not reported since upgrading to PyMongo 3. Closes #680. - Fix: when a document is soft deleted, the OPLOG `_updated` field is not the time of the deletion but the time of the previous last update (Cyril Bonnard). diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index f075818bd..00b48ebcb 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -404,23 +404,29 @@ def insert(self, resource, doc_or_docs): doc_or_docs = [doc_or_docs] try: - return coll.insert_many(doc_or_docs).inserted_ids - except pymongo.errors.DuplicateKeyError as e: - abort(409, description=debug_error_message( - 'pymongo.errors.DuplicateKeyError: %s' % e - )) - except pymongo.errors.InvalidOperation as e: - self.app.logger.exception(e) - abort(500, description=debug_error_message( - 'pymongo.errors.InvalidOperation: %s' % e - )) - except pymongo.errors.OperationFailure as e: - # most likely a 'w' (write_concern) setting which needs an - # existing ReplicaSet which doesn't exist. Please note that the - # update will actually succeed (a new ETag will be needed). + return coll.insert_many(doc_or_docs, ordered=True).inserted_ids + except pymongo.errors.BulkWriteError as e: self.app.logger.exception(e) + + # since this is an ordered bulk operation, all remaining inserts + # are aborted. Be aware that if BULK_ENABLED is True and more than + # one document is included with the payload, some documents might + # have been successfully inserted, even if the operation was + # aborted. + + # report a duplicate key error since this can probably be + # handled by the client. + for error in e.details['writeErrors']: + # amazingly enough, pymongo does not appear to be exposing + # error codes as constants. + if error['code'] == 11000: + abort(409, description=debug_error_message( + 'Duplicate key error at index: %s, message: %s' % ( + error['index'], error['errmsg']) + )) + abort(500, description=debug_error_message( - 'pymongo.errors.OperationFailure: %s' % e + 'pymongo.errors.BulkWriteError: %s' % e )) def _change_request(self, resource, id_, changes, original, replace=False): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 94e92fcec..4e405fad6 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -55,6 +55,15 @@ def test_post_string(self): data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) + def test_post_duplicate_key(self): + data = {'ref': '1234567890123456789054321'} + r = self.perform_post(data) + id_field = self.domain[self.known_resource]['id_field'] + item_id = r[id_field] + data = {'ref': '0123456789012345678901234', id_field: item_id} + r, status = self.post(self.known_resource_url, data=data) + self.assertEqual(status, 409) + def test_post_integer(self): del(self.domain['contacts']['schema']['ref']['required']) test_field = 'prog' From 06273f995abff81059cfbc21d0e1cae9405e800f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 26 Feb 2016 07:31:59 +0100 Subject: [PATCH 052/821] Revert "Ensure uniqueness of (custom) id fields" Addresses #788. This reverts commit 48362bc20c928ccad57fe3fad8d72f9e23412723. Conflicts: eve/io/mongo/validation.py --- CHANGES | 2 ++ eve/flaskapp.py | 19 +++++-------------- eve/io/mongo/validation.py | 12 +----------- eve/tests/config.py | 3 +-- eve/tests/endpoints.py | 2 +- eve/tests/io/mongo.py | 25 +------------------------ eve/tests/methods/post.py | 3 +-- eve/tests/test_settings.py | 3 +-- eve/tests/versioning.py | 1 - 9 files changed, 13 insertions(+), 57 deletions(-) diff --git a/CHANGES b/CHANGES index 539eeabb2..ee19b9a79 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: Remove "ensure uniqueness of (custom) id fields" feature. Addresses + #788. - Fix: ``409 Conflict`` not reported since upgrading to PyMongo 3. Closes #680. - Fix: when a document is soft deleted, the OPLOG `_updated` field is not the time of the deletion but the time of the previous last update (Cyril diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 8e183687c..9038c9ca9 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -390,12 +390,6 @@ def validate_schema(self, resource, schema): """ resource_settings = self.config['DOMAIN'][resource] - # ensure id_field is defined as unique - id_field = resource_settings['id_field'] - if 'unique' not in schema[id_field] or not schema[id_field]['unique']: - raise SchemaException("'unique' key is mandatory for id field " - "'%s'" % id_field) - # ensure automatically handled fields aren't defined fields = [eve.DATE_CREATED, eve.LAST_UPDATED, eve.ETAG] @@ -664,14 +658,11 @@ def set_schema_defaults(self, schema, id_field): .. versionadded: 0.0.5 """ - # id_field has to be 'unique'. Some data layers, e.g. the default mongo - # layer ignore this validation rule to avoid a performance hit (with - # 'unique' rule set, we would end up with an extra db loopback on every - # insert). - schema.setdefault(id_field, { - 'type': 'objectid', - 'unique': True - }) + # Don't set id_field 'unique' since we already handle + # DuplicateKeyConflict in the mongo layer. This also + # avoids a performance hit (with 'unique' rule set, we would + # end up with an extra db loopback on every insert). + schema.setdefault(id_field, {'type': 'objectid'}) # set default 'field' value for all 'data_relation' rulesets, however # nested diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 87d6331e0..becbd227f 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -56,7 +56,7 @@ def __init__(self, schema=None, resource=None, allow_unknown=False, self.resource = resource self._id = None self._original_document = None - schema = self._remove_unique_rules_on_fields_with_unique_index(schema) + if resource: transparent_schema_rules = \ config.DOMAIN[resource]['transparent_schema_rules'] @@ -66,16 +66,6 @@ def __init__(self, schema=None, resource=None, allow_unknown=False, transparent_schema_rules=transparent_schema_rules, allow_unknown=allow_unknown) - def _remove_unique_rules_on_fields_with_unique_index(self, schema): - # TODO: Actually do what the function name suggests. This version just - # removes the unique constraint on _id. We could use the information - # available by app.data.driver.db[datasource].index_information() to - # remove all unnecessary unique constraints. - result = copy.deepcopy(schema) - if '_id' in result and 'unique' in result['_id']: - del(result['_id']['unique']) - return result - def validate_update(self, document, _id, original_document=None): """ Validate method to be invoked when performing an update, not an insert. diff --git a/eve/tests/config.py b/eve/tests/config.py index e2c0f92ee..087f15112 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -175,8 +175,7 @@ def test_set_schema_defaults(self): self.domain['contacts']['id_field']) id_field = self.domain['invoices']['id_field'] self.assertTrue(id_field in schema) - self.assertEqual(schema[id_field], - {'type': 'objectid', 'unique': True}) + self.assertEqual(schema[id_field], {'type': 'objectid'}) def test_set_defaults(self): self.domain.clear() diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index f6dcb32bc..bd94effab 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -64,7 +64,7 @@ def setUp(self): 'item_methods': ['GET', 'PATCH', 'PUT', 'DELETE'], 'item_url': 'uuid', 'schema': { - '_id': {'type': 'uuid', 'unique': True}, + '_id': {'type': 'uuid'}, 'name': {'type': 'string'} } } diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index 4f0c41d80..a66544672 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -7,6 +7,7 @@ from eve.io.mongo.parser import parse, ParseError from eve.io.mongo import Validator, Mongo, MongoJSONEncoder from eve.tests import TestBase +from eve.utils import config from eve.tests.test_settings import MONGO_DBNAME import simplejson as json @@ -295,30 +296,6 @@ def test_dependencies_with_defaults(self): v = Validator(schema) self.assertTrue(v.validate(doc)) - def test_removal_of_unnecessary_unique_constraints(self): - schema = { - '_id': { - 'type': 'objectid', - 'unique': True - }, - 'foo': { - 'type': 'string', - 'minlength': 2 - } - } - expected_schema = { - '_id': { - 'type': 'objectid' - }, - 'foo': { - 'type': 'string', - 'minlength': 2 - } - } - v = Validator(schema) - schema = v._remove_unique_rules_on_fields_with_unique_index(schema) - self.assertEqual(expected_schema, schema) - class TestMongoDriver(TestBase): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 4e405fad6..a388ede3c 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -695,8 +695,7 @@ def test_post_nested(self): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) values = self.compare_post_with_get( - r[self.domain[self.known_resource]['id_field']], - ['location']).pop() + r[self.domain[self.known_resource]['id_field']], ['location']).pop() self.assertEqual(values['city'], 'a nested city') self.assertEqual(values['address'], 'a nested address') diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 66136a773..d72ad6280 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -284,8 +284,7 @@ 'schema': { 'sku': { 'type': 'string', - 'maxlength': 16, - 'unique': True + 'maxlength': 16 }, 'title': { 'type': 'string', diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index a85761a45..bd436cfeb 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -1355,7 +1355,6 @@ def setUp(self): super(TestVersioningWithCustomIdField, self).setUp() self.domain[self.known_resource]['schema'][self.id_field] = { 'type': 'string', - 'unique': True } self.enableVersioning() self.insertTestData() From 072a23c7f5a4625f84feff76723f74d2060932e5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 26 Feb 2016 08:22:05 +0100 Subject: [PATCH 053/821] Small refactoring and flake8 fixes --- eve/tests/io/mongo.py | 10 +++++----- eve/tests/methods/post.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index a66544672..fc3bd7706 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- +from datetime import datetime -from unittest import TestCase +import simplejson as json from bson import ObjectId -from datetime import datetime from cerberus import SchemaError -from eve.io.mongo.parser import parse, ParseError +from unittest import TestCase + from eve.io.mongo import Validator, Mongo, MongoJSONEncoder +from eve.io.mongo.parser import parse, ParseError from eve.tests import TestBase -from eve.utils import config from eve.tests.test_settings import MONGO_DBNAME -import simplejson as json class TestPythonParser(TestCase): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index a388ede3c..4e405fad6 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -695,7 +695,8 @@ def test_post_nested(self): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) values = self.compare_post_with_get( - r[self.domain[self.known_resource]['id_field']], ['location']).pop() + r[self.domain[self.known_resource]['id_field']], + ['location']).pop() self.assertEqual(values['city'], 'a nested city') self.assertEqual(values['address'], 'a nested address') From 22f56958edbbd82aab94134842207d570c2f63a5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 27 Feb 2016 08:53:05 +0100 Subject: [PATCH 054/821] Do not allow '$' and '.' in field names. Closes #780. --- CHANGES | 2 ++ eve/flaskapp.py | 17 ++++++++++++++++- eve/tests/config.py | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ee19b9a79..8ece15a0d 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: Mongo does not allow ``$`` and ``.`` in field names. Apply this + validation in schemas and dict fields. Closes #780. - Fix: Remove "ensure uniqueness of (custom) id fields" feature. Addresses #788. - Fix: ``409 Conflict`` not reported since upgrading to PyMongo 3. Closes #680. diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 9038c9ca9..e98a619a6 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -363,6 +363,9 @@ def validate_schema(self, resource, schema): :param resource: resource name. :param schema: schema definition for the resource. + .. versionchanged:: 0.6.2 + Do not allow '$' and '.' in root and dict field names. #780. + .. versionchanged:: 0.6 ID_FIELD in the schema is not an offender anymore. @@ -388,6 +391,13 @@ def validate_schema(self, resource, schema): Now collecting offending items in a list and inserting results into the exception message. """ + def validate_field_name(field): + forbidden = ['$', '.'] + if any(x in field for x in forbidden): + raise SchemaException( + "Field '%s' cannot contain any of the following: '%s'." % + (field, ', '.join(forbidden))) + resource_settings = self.config['DOMAIN'][resource] # ensure automatically handled fields aren't defined @@ -411,8 +421,13 @@ def validate_schema(self, resource, schema): '(they will be handled automatically).' % (', '.join(offenders), resource)) - # check data_relation rules for field, ruleset in schema.items(): + validate_field_name(field) + if 'dict' in ruleset.get('type', ''): + for field in ruleset.get('schema', {}).keys(): + validate_field_name(field) + + # check data_relation rules if 'data_relation' in ruleset: if 'resource' not in ruleset['data_relation']: raise SchemaException("'resource' key is mandatory for " diff --git a/eve/tests/config.py b/eve/tests/config.py index 087f15112..441a79012 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -166,6 +166,25 @@ def test_validate_schema(self): del(schema['person']['data_relation']['resource']) self.assertValidateSchemaFailure('invoices', schema, 'resource') + def test_validate_invalid_field_names(self): + schema = self.domain['invoices']['schema'] + schema['te$t'] = {'type': 'string'} + self.assertValidateSchemaFailure('invoices', schema, 'te$t') + del(schema['te$t']) + + schema['te.t'] = {'type': 'string'} + self.assertValidateSchemaFailure('invoices', schema, 'te.t') + del(schema['te.t']) + + schema['test_a_dict_schema'] = { + 'type': 'dict', + 'schema': {'te$t': {'type': 'string'}} + } + self.assertValidateSchemaFailure('invoices', schema, 'te$t') + + schema['test_a_dict_schema']['schema'] = {'te.t': {'type': 'string'}} + self.assertValidateSchemaFailure('invoices', schema, 'te.t') + def test_set_schema_defaults(self): # default data_relation field value schema = self.domain['invoices']['schema'] From 2fa54fd4d8e17e728f582ce0ec32a7516fb056ed Mon Sep 17 00:00:00 2001 From: Hamdy Date: Tue, 1 Mar 2016 12:06:46 +0200 Subject: [PATCH 055/821] Added missing imports in authentication docs --- docs/authentication.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index 861a77712..d79c1de5d 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -219,7 +219,7 @@ resources/methods will be secured unless they are made explicitly public. import bcrypt from eve import Eve from eve.auth import BasicAuth - + from flask import current_app as app class BCryptAuth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): @@ -260,7 +260,7 @@ resources/methods will be secured unless they are made explicitly public. from eve import Eve from eve.auth import BasicAuth from werkzeug.security import check_password_hash - + from flask import current_app as app class Sha1Auth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): @@ -304,7 +304,7 @@ resources and/or methods to public access -see docs). from eve import Eve from eve.auth import TokenAuth - + from flask import current_app as app class TokenAuth(TokenAuth): def check_auth(self, token, allowed_roles, resource, method): @@ -371,6 +371,7 @@ Eve `repository`_. from eve import Eve from eve.auth import HMACAuth + from flask import current_app as app from hashlib import sha1 import hmac @@ -440,7 +441,7 @@ unless they are made explicitly public. from eve import Eve from eve.auth import BasicAuth from werkzeug.security import check_password_hash - + from flask import current_app as app class RolesAuth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): From 71c0226d3d9ad635e96798fd54145a50a19682c6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 2 Mar 2016 07:43:49 +0100 Subject: [PATCH 056/821] Changelog update for #825. Add Hamdy to authors. Conflicts: AUTHORS --- AUTHORS | 2 ++ CHANGES | 1 + 2 files changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index f0d426b5d..026664fff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -42,6 +42,8 @@ Patches and Contributions - Gino Zhang - Gonéri Le Bouder - Grisha K. +- Gustavo Vargas +- Hamdy - Hannes Tiede - Harro van der Klauw - Henrique Barroso diff --git a/CHANGES b/CHANGES index 8ece15a0d..01314ec39 100644 --- a/CHANGES +++ b/CHANGES @@ -46,6 +46,7 @@ Version 0.6.2 - Update: Flask-PyMongo 0.4+ is now required. - Docs: fix some typos (Manquer, Patrick Decat). +- Docs: add missing imports to authentication docs (Hamdy) - Update license to 2016 (Prayag Verma) From c517c11c71a357445808d182d4d1f2d4702cddff Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Mar 2016 18:28:12 +0100 Subject: [PATCH 057/821] Fix: unique rule is checked against soft deleted documents Closes #831. --- CHANGES | 2 ++ docs/features.rst | 8 ++++++++ eve/io/mongo/validation.py | 22 ++++++++++++++++++++- eve/tests/methods/delete.py | 38 +++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 01314ec39..61bcb32fe 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: ``unique`` validation rule is checked against soft deleted documents. + Closes #831. - Fix: Mongo does not allow ``$`` and ``.`` in field names. Apply this validation in schemas and dict fields. Closes #780. - Fix: Remove "ensure uniqueness of (custom) id fields" feature. Addresses diff --git a/docs/features.rst b/docs/features.rst index 3158992d8..234bf57bf 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -982,6 +982,14 @@ specified, or an empty request would be made to restore the document as is. The request must be made with proper authorization for write permission to the soft deleted document or it will be refused. +Be aware that, should a previously soft deleted document be restored, there is +a chance that an eventual unique field might end up being now duplicated in two +different documents: the restored one, and another which might have been stored +with the same field value while the original (now restored) was in 'deleted' +state. This is because soft deleted documents are ignored when validating the +`unique` rule for new or updated documents. + + Versioning ~~~~~~~~~~ Soft deleting a versioned document creates a new version of that document with diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index becbd227f..5afad7072 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -146,14 +146,34 @@ def _validate_unique(self, unique, field, value): def _is_value_unique(self, unique, field, value, query): """ Validates that a field value is unique. + .. versionchanged:: 0.6.2 + Exclude soft deleted documents from uniqueness check. Closes #831. + .. versionadded:: 0.6 """ if unique: query[field] = value + resource_config = config.DOMAIN[self.resource] + + # exclude soft deleted documents if applicable + if resource_config['soft_delete']: + # be aware that, should a previously (soft) deleted document be + # restored, and because we explicitly ignore soft deleted + # documents while validating 'unique' fields, there is a chance + # that a unique field value will end up being now duplicated + # in two documents: the restored one, and the one which has + # been stored with the same field value while the original + # document was in 'deleted' state. + + # we make sure to also include documents which are missing the + # DELETED field. This happens when soft deletes are enabled on + # an a resource with existing documents. + query[config.DELETED] = {'$ne': True} + # exclude current document if self._id: - id_field = config.DOMAIN[self.resource]['id_field'] + id_field = resource_config['id_field'] query[id_field] = {'$ne': self._id} # we perform the check on the native mongo driver (and not on diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 0aa5f9738..e002f89c6 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -578,6 +578,44 @@ def test_exclusive_projection(self): data, status = self.parse_response(r) self.assert200(status) + def test_exclude_soft_deleted_documents_from_unique_checks(self): + """ Test that soft deleted documents are ignored when validating new + documents against the 'unique' rule. See #831. + """ + unique_value = "1234567890123456789054321" + + # 'ref' field has a 'unique' rule applied to it. + r = self.test_client.post(self.known_resource_url, data={ + 'ref': unique_value + }) + data, status = self.parse_response(r) + self.assert201(status) + new_item_id = data[self.domain[self.known_resource]['id_field']] + new_item_etag = data[self.app.config['ETAG']] + + # we can't post a new document with the same value. + r = self.test_client.post(self.known_resource_url, data={ + 'ref': unique_value + }) + data, status = self.parse_response(r) + self.assert422(status) + + # we now soft delete the document. + r = self.test_client.delete( + self.known_resource_url + "/" + new_item_id, + headers=[('If-Match', new_item_etag)] + ) + data, status = self.parse_response(r) + self.assert204(status) + + # posting a new document with the same value for 'ref' + # is now possible. + r = self.test_client.post(self.known_resource_url, data={ + 'ref': unique_value + }) + data, status = self.parse_response(r) + self.assert201(status) + class TestResourceSpecificSoftDelete(TestBase): def setUp(self): From 4ff291737fe5d2db59b0a8185f6d23689e90d242 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 12 Mar 2016 08:39:57 +0100 Subject: [PATCH 058/821] Rename Access-Control-Allow-Max-Age to Access-Control-Max-Age Closes #829. --- CHANGES | 2 ++ eve/render.py | 2 +- eve/tests/renders.py | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 61bcb32fe..94f115007 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ In Development Version 0.6.2 ~~~~~~~~~~~~~ +- Fix: ``Access-Control-Allow-Max-Age`` should actually be + ``Access-Control-Max-Age``. Closes #829. - Fix: ``unique`` validation rule is checked against soft deleted documents. Closes #831. - Fix: Mongo does not allow ``$`` and ``.`` in field names. Apply this diff --git a/eve/render.py b/eve/render.py index 9f0203d77..826d2428f 100644 --- a/eve/render.py +++ b/eve/render.py @@ -219,7 +219,7 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, resp.headers.add('Access-Control-Expose-Headers', ', '.join(expose_headers)) resp.headers.add('Access-Control-Allow-Methods', methods) - resp.headers.add('Access-Control-Allow-Max-Age', config.X_MAX_AGE) + resp.headers.add('Access-Control-Max-Age', config.X_MAX_AGE) if allow_credentials: resp.headers.add('Access-Control-Allow-Credentials', "true") diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 2716d8503..ed5336ca4 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -118,7 +118,7 @@ def test_CORS(self): r = self.test_client.get('/') self.assertFalse('Access-Control-Allow-Origin' in r.headers) self.assertFalse('Access-Control-Allow-Methods' in r.headers) - self.assertFalse('Access-Control-Allow-Max-Age' in r.headers) + self.assertFalse('Access-Control-Max-Age' in r.headers) self.assertFalse('Access-Control-Expose-Headers' in r.headers) self.assertFalse('Access-Control-Allow-Credentials' in r.headers) self.assert200(r.status_code) @@ -172,20 +172,20 @@ def test_CORS(self): # other Access-Control-Allow- headers are included. self.assertTrue('Access-Control-Allow-Headers' in r.headers) self.assertTrue('Access-Control-Allow-Methods' in r.headers) - self.assertTrue('Access-Control-Allow-Max-Age' in r.headers) + self.assertTrue('Access-Control-Max-Age' in r.headers) self.assertTrue('Access-Control-Expose-Headers' in r.headers) def test_CORS_MAX_AGE(self): self.app.config['X_DOMAINS'] = '*' r = self.test_client.get('/', headers=[('Origin', 'http://example.com')]) - self.assertEqual(r.headers['Access-Control-Allow-Max-Age'], + self.assertEqual(r.headers['Access-Control-Max-Age'], '21600') self.app.config['X_MAX_AGE'] = 2000 r = self.test_client.get('/', headers=[('Origin', 'http://example.com')]) - self.assertEqual(r.headers['Access-Control-Allow-Max-Age'], + self.assertEqual(r.headers['Access-Control-Max-Age'], '2000') def test_CORS_OPTIONS(self, url='/', methods=None): @@ -195,7 +195,7 @@ def test_CORS_OPTIONS(self, url='/', methods=None): r = self.test_client.open(url, method='OPTIONS') self.assertFalse('Access-Control-Allow-Origin' in r.headers) self.assertFalse('Access-Control-Allow-Methods' in r.headers) - self.assertFalse('Access-Control-Allow-Max-Age' in r.headers) + self.assertFalse('Access-Control-Max-Age' in r.headers) self.assertFalse('Access-Control-Expose-Headers' in r.headers) self.assertFalse('Access-Control-Allow-Credentials' in r.headers) self.assert200(r.status_code) @@ -245,7 +245,7 @@ def test_CORS_OPTIONS(self, url='/', methods=None): self.assertTrue(m in r.headers['Access-Control-Allow-Methods']) self.assertTrue('Access-Control-Allow-Origin' in r.headers) - self.assertTrue('Access-Control-Allow-Max-Age' in r.headers) + self.assertTrue('Access-Control-Max-Age' in r.headers) self.assertTrue('Access-Control-Expose-Headers' in r.headers) r = self.test_client.get(url, headers=[('Origin', From 26ae1095fc359cd4d582a1b70a65585d72bbc2d7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 Mar 2016 09:50:14 +0100 Subject: [PATCH 059/821] Bump PyMongo to 3.2, Werkzeug to 0.11.4, simplejson to 3.8.2 --- CHANGES | 4 +++- dev-requirements.txt | 1 + requirements.txt | 8 ++++---- setup.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 94f115007..97b7d5f7c 100644 --- a/CHANGES +++ b/CHANGES @@ -46,8 +46,10 @@ Version 0.6.2 - Fix: When ``SOFT_DELETE`` is active an exclusive ``datasource.projection`` causes a ``500`` error. Closes #752. -- Update: PyMongo 3.1 is now required. +- Update: PyMongo 3.2 is now required. - Update: Flask-PyMongo 0.4+ is now required. +- Update: Werkzeug up to 0.11.4 is now required +- Change: simplejson v3.8.2 is now required. - Docs: fix some typos (Manquer, Patrick Decat). - Docs: add missing imports to authentication docs (Hamdy) diff --git a/dev-requirements.txt b/dev-requirements.txt index bfa3c42ab..d98db9bfd 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,6 +3,7 @@ flake8==2.3.0 mccabe==0.3 pep8==1.5.7 pip-tools==0.3.5 +pip-review==0.4 py==1.4.26 pyflakes==0.8.1 Pygments==2.0.1 diff --git a/requirements.txt b/requirements.txt index e125004de..a11ca110c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ Cerberus==0.9.2 Events==0.2.1 -Flask-PyMongo==0.4.0 +Flask-PyMongo==0.4.1 Flask==0.10.1 itsdangerous==0.24 Jinja2==2.7.3 MarkupSafe==0.23 -pymongo==3.1 -simplejson==3.6.5 -Werkzeug==0.10.1 +pymongo==3.2.1 +simplejson==3.8.2 +Werkzeug==0.11.4 diff --git a/setup.py b/setup.py index c7caa7a8f..977927d90 100755 --- a/setup.py +++ b/setup.py @@ -9,12 +9,12 @@ 'cerberus>=0.9.2,<0.10', 'events>=0.2.1,<0.3', 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<0.11', + 'werkzeug>=0.9.4,<0.11.4', 'markupsafe>=0.23,<1.0', 'jinja2>=2.7.2,<3.0', 'itsdangerous>=0.22,<1.0', 'flask>=0.10.1,<0.11', - 'pymongo>=3.1', + 'pymongo>=3.2', 'flask-pymongo>=0.4', ] From e0fb4432e5ed00b52a9f16f23cfac605e5b92358 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 Mar 2016 10:11:30 +0100 Subject: [PATCH 060/821] v0.6.2 release date --- CHANGES | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 97b7d5f7c..108d8858a 100644 --- a/CHANGES +++ b/CHANGES @@ -3,12 +3,14 @@ Changelog Here you can see the full list of changes between each Eve release. -In Development --------------- +Stable +------ Version 0.6.2 ~~~~~~~~~~~~~ +Released on 14 March, 2016 + - Fix: ``Access-Control-Allow-Max-Age`` should actually be ``Access-Control-Max-Age``. Closes #829. - Fix: ``unique`` validation rule is checked against soft deleted documents. @@ -55,10 +57,6 @@ Version 0.6.2 - Docs: add missing imports to authentication docs (Hamdy) - Update license to 2016 (Prayag Verma) - -Stable ------- - Version 0.6.1 ~~~~~~~~~~~~~ From 5db6ddeff01cd7bd1686c987761b0fdacf7f1245 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 Mar 2016 10:11:39 +0100 Subject: [PATCH 061/821] Bump version to 0.6.2 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index bbb5a339e..378eb29c9 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.6.2.dev0' +__version__ = '0.6.2' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 977927d90..fe5dfa31f 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.6.2.dev0', + version='0.6.2', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 510f39173da44337984121589b3b744fcb1e9dc6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 16 Mar 2016 10:22:16 +0100 Subject: [PATCH 062/821] Fix: static projections are not honoured since v0.6.2 Closes #837. Conflicts: CHANGES eve/flaskapp.py --- CHANGES | 5 +++++ eve/flaskapp.py | 4 +++- eve/tests/methods/get.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 108d8858a..d6ef6f97c 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,11 @@ Here you can see the full list of changes between each Eve release. Stable ------ +Version 0.6.3 +~~~~~~~~~~~~~ + +- Fix: Since 0.6.2, static projections are not honoured. Closes #837. + Version 0.6.2 ~~~~~~~~~~~~~ diff --git a/eve/flaskapp.py b/eve/flaskapp.py index e98a619a6..56625b75b 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -618,6 +618,9 @@ def _set_resource_defaults(self, resource, settings): # be rejected by Mongo if not exclusion and len(schema) and \ settings['allow_unknown'] is False: + if not projection: + projection.update(dict((field, 1) for (field) in schema)) + # enable retrieval of actual schema fields only. Eventual db # fields not included in the schema won't be returned. # despite projection, automatic fields are always included. @@ -630,7 +633,6 @@ def _set_resource_defaults(self, resource, settings): projection[ settings['id_field'] + self.config['VERSION_ID_SUFFIX']] = 1 - projection.update(dict((field, 1) for (field) in schema)) else: # all fields are returned. projection = None diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 801e02681..601d26941 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -278,6 +278,28 @@ def test_get_projection(self): self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + def test_get_static_projection(self): + """ Test that static projections are honoured """ + response, status = self.get(self.different_resource) + self.assert200(status) + + resource = response['_items'] + + # 'users' has a static inclusive projection with 'username' and 'ref' + # fields, so other document fields should be excluded. + for r in resource: + self.assertFalse('location' in r) + self.assertFalse('role' in r) + self.assertFalse('prog' in r) + self.assertTrue('username' in r) + self.assertTrue('ref' in r) + self.assertTrue(self.domain[self.known_resource]['id_field'] in r) + self.assertTrue(self.app.config['ETAG'] in r) + self.assertTrue(self.app.config['LAST_UPDATED'] in r) + self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + def test_get_custom_projection(self): self.app.config['QUERY_PROJECTION'] = 'view' projection = '{"prog": 1}' From b92cf70dc8ae82f6491c8ffb1d33a6a51f2679e8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 16 Mar 2016 10:52:54 +0100 Subject: [PATCH 063/821] Bump version to 0.6.3 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 378eb29c9..c8b79af5a 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.6.2' +__version__ = '0.6.3' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index fe5dfa31f..d5c78fa85 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.6.2', + version='0.6.3', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 8ff79f39c9cf9f678f6466bb7e97289fa77804b3 Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Fri, 4 Nov 2016 16:03:29 +0100 Subject: [PATCH 064/821] Fixed test Issue #934 --- eve/tests/versioning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 9e781a551..f57e5d80a 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -530,7 +530,8 @@ def test_getitem_version_diffs(self): self.assertEqualFields(self.item_change, items[1], self.fields) changed_fields = self.fields + [ self.version_field, - self.app.config['ETAG']] + self.app.config['ETAG'], + self.app.config['LINKS']] for field in changed_fields: self.assertTrue(field in items[1], "%s not in diffs" % field) # since the test routine happens so fast, `LAST_UPDATED` may or may not From 9329a1498843fb213642fc2b7cdd5c6b910d53e7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 5 Nov 2016 08:10:41 +0100 Subject: [PATCH 065/821] Changelog for #935 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index ec66002c0..b61ba3f34 100644 --- a/CHANGES +++ b/CHANGES @@ -74,6 +74,8 @@ Version 0.7 - Change: ETag response header now conforms to RFC 7232/2.3 and is surrounded by double quotes. Closes #794. +- Fix: fix intermittently failing test. Closes #934 (Conrad Burchert). + - Fix: Multiple, fast (within a 1 second window) and neutral (no actual changes) PATCH requests should not raise ``412 Precondition Failed``. Closes #920. From 233f068bc858d5f638eed199d5dfd2b1132526ac Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 5 Nov 2016 08:10:55 +0100 Subject: [PATCH 066/821] Conrad Burchert --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 461784f23..cf3733c7c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Patches and Contributions - Christian Henke - Christoph Witzany - Christopher Larsen +- Conrad Burchert - Cyprien Pannier - Cyril Bonnard - Daniel Lytkin From c9db27983f47cdd896b2444ef24ba892fd05febf Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Fri, 4 Nov 2016 11:29:58 +0100 Subject: [PATCH 067/821] Allow posting lists by using the same key multiple times HTTP allows multiple values with the same key in application/x-www-form-urlencoded and multipart/form-data content types. We can use this feature to send a list using for example an HTML form. This is very useful to send a list of files. This commit adds a config setting AUTO_COLLAPSE_MULTI_KEYS to enable automatic conversion of data, which has the same key, to a list of values. See issue #932 --- eve/default_settings.py | 1 + eve/flaskapp.py | 5 +- eve/methods/common.py | 156 +++++++++++++++++++++++--------------- eve/tests/methods/post.py | 69 +++++++++++++++++ 4 files changed, 170 insertions(+), 61 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index 9e7791909..b78aab929 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -202,6 +202,7 @@ MEDIA_BASE_URL = None MULTIPART_FORM_FIELDS_AS_JSON = False +AUTO_COLLAPSE_MULTI_KEYS = False SCHEMA_ENDPOINT = None diff --git a/eve/flaskapp.py b/eve/flaskapp.py index d603fd3ff..7f521755d 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -699,7 +699,10 @@ def _set_resource_projection(self, ds, schema, settings): # list of all media fields for the resource settings['_media'] = [field for field, definition in schema.items() if - definition.get('type') == 'media'] + definition.get('type') == 'media' or + (definition.get('type') == 'list' and + definition.get('schema', {}).get('type') + == 'media')] if settings['_media'] and not self.media: raise ConfigException('A media storage class of type ' diff --git a/eve/methods/common.py b/eve/methods/common.py index 3d08d5687..043e4ab58 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -30,6 +30,7 @@ from flask import g from flask import request from functools import wraps +from werkzeug.datastructures import MultiDict, CombinedMultiDict def get_document(resource, concurrency_check, **lookup): @@ -163,7 +164,7 @@ def payload(): if content_type == 'application/json': return request.get_json() elif content_type == 'application/x-www-form-urlencoded': - return request.form.to_dict() if len(request.form) else \ + return multidict_to_dict(request.form) if len(request.form) else \ abort(400, description='No form-urlencoded data supplied') elif content_type == 'multipart/form-data': # as multipart is also used for file uploads, we let an empty @@ -173,24 +174,20 @@ def payload(): # merge form fields and request files, so we get a single payload # to be validated against the resource schema. - if config.MULTIPART_FORM_FIELDS_AS_JSON: - - formItems = dict(list(request.form.to_dict().items())) - - for key in formItems.keys(): - try: - formItems[key] = json.loads(formItems[key]) - except ValueError: - formItems[key] = json.loads( - '"{0}"'.format(formItems[key])) + formItems = MultiDict(request.form) - return dict(list(formItems.items()) + - list(request.files.to_dict().items())) - else: - # list() is needed because Python3 items() returns a - # dict_view, not a list as in Python2. - return dict(list(request.form.to_dict().items()) + - list(request.files.to_dict().items())) + if config.MULTIPART_FORM_FIELDS_AS_JSON: + for key, lst in formItems.lists(): + new_lst = [] + for value in lst: + try: + new_lst.append(json.loads(value)) + except ValueError: + new_lst.append(json.loads('"{0}"'.format(value))) + formItems.setlist(key, new_lst) + + payload = CombinedMultiDict([formItems, request.files]) + return multidict_to_dict(payload) else: abort(400, description='No multipart/form-data supplied') @@ -198,6 +195,21 @@ def payload(): abort(400, description='Unknown or no Content-Type header supplied') +def multidict_to_dict(multidict): + """ Convert a MultiDict containing form data into a regular dict. If the + config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the + same key get entered as a list. If it is False, the first entry is picked. + """ + if config.AUTO_COLLAPSE_MULTI_KEYS: + d = dict(multidict.lists()) + for key, value in d.items(): + if len(value) == 1: + d[key] = value[0] + return d + else: + return multidict.to_dict() + + class RateLimit(object): """ Implements the Rate-Limiting logic using Redis as a backend. @@ -788,43 +800,55 @@ def resolve_media_files(document, resource): .. versionadded:: 0.4 """ for field in resource_media_fields(document, resource): - file_id = document[field] - _file = app.media.get(file_id, resource) - - if _file: - # otherwise we have a valid file and should send extended response - # start with the basic file object - if config.RETURN_MEDIA_AS_BASE64_STRING: - ret_file = base64.encodestring(_file.read()) - elif config.RETURN_MEDIA_AS_URL: - prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ - is not None else app.api_prefix - ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, - file_id) - else: - ret_file = None - - if config.EXTENDED_MEDIA_INFO: - document[field] = { - 'file': ret_file, - } - - # check if we should return any special fields - for attribute in config.EXTENDED_MEDIA_INFO: - if hasattr(_file, attribute): - # add extended field if found in the file object - document[field].update({ - attribute: getattr(_file, attribute) - }) - else: - # tried to select an invalid attribute - abort(500, description=debug_error_message( - 'Invalid extended media attribute requested' - )) - else: - document[field] = ret_file + if isinstance(document[field], list): + resolved_list = [] + for file_id in document[field]: + resolved_list.append(resolve_one_media(file_id, resource)) + document[field] = resolved_list + else: + document[field] = resolve_one_media(document[field], resource) + + +def resolve_one_media(file_id, resource): + """ Get response for one media file """ + _file = app.media.get(file_id, resource) + + if _file: + # otherwise we have a valid file and should send extended response + # start with the basic file object + if config.RETURN_MEDIA_AS_BASE64_STRING: + ret_file = base64.encodestring(_file.read()) + elif config.RETURN_MEDIA_AS_URL: + prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ + is not None else app.api_prefix + ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, + file_id) else: - document[field] = None + ret_file = None + + if config.EXTENDED_MEDIA_INFO: + ret = { + 'file': ret_file, + } + + # check if we should return any special fields + for attribute in config.EXTENDED_MEDIA_INFO: + if hasattr(_file, attribute): + # add extended field if found in the file object + ret.update({ + attribute: getattr(_file, attribute) + }) + else: + # tried to select an invalid attribute + abort(500, description=debug_error_message( + 'Invalid extended media attribute requested' + )) + + return ret + else: + return ret_file + else: + return None def marshal_write_response(document, resource): @@ -877,15 +901,27 @@ def store_media_files(document, resource, original=None): for field in resource_media_fields(document, resource): if original and field in original: # since file replacement is not supported by the media storage - # system, we first need to delete the file being replaced. - app.media.delete(original[field], resource) + # system, we first need to delete the files being replaced. + if isinstance(original[field], list): + for file_id in original[field]: + app.media.delete(file_id, resource) + else: + app.media.delete(original[field], resource) if document[field]: - # store file and update document with file's unique id/filename + # store files and update document with file's unique id/filename # also pass in mimetype for use when retrieving the file - document[field] = app.media.put( - document[field], filename=document[field].filename, - content_type=document[field].mimetype, resource=resource) + if isinstance(document[field], list): + id_lst = [] + for stor_obj in document[field]: + id_lst.append(app.media.put( + stor_obj, filename=stor_obj.filename, + content_type=stor_obj.mimetype, resource=resource)) + document[field] = id_lst + else: + document[field] = app.media.put( + document[field], filename=document[field].filename, + content_type=document[field].mimetype, resource=resource) def resource_media_fields(document, resource): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 2e4070231..56c206631 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1,3 +1,6 @@ +from base64 import b64decode +from bson import ObjectId + import simplejson as json from eve.tests import TestBase @@ -8,6 +11,9 @@ from eve.methods.post import post from eve.methods.post import post_internal +from io import BytesIO + +from werkzeug.datastructures import MultiDict class TestPost(TestBase): def test_unknown_resource(self): @@ -237,6 +243,69 @@ def test_post_x_www_form_urlencoded_number_serialization(self): self.assertTrue('OK' in r[STATUS]) self.assertPostResponse(r) + def test_post_auto_collapse_multiple_keys(self): + self.app.config['AUTO_COLLAPSE_MULTI_KEYS'] = True + self.app.register_resource('test_res', { + 'schema': { + 'list_field': { + 'type': 'list', + 'schema': { + 'type': 'string' + } + } + } + }) + + data = MultiDict([("list_field", "value1"), + ("list_field", "value2")]) + resp = self.test_client.post( + '/test_res/', data=data, + content_type='application/x-www-form-urlencoded') + r, status = self.parse_response(resp) + self.assert201(status) + + resp = self.test_client.post('/test_res/', data=data, + content_type='multipart/form-data') + r, status = self.parse_response(resp) + self.assert201(status) + + def test_post_auto_collapse_media_list(self): + self.app.config['AUTO_COLLAPSE_MULTI_KEYS'] = True + self.app.register_resource('test_res', { + 'schema': { + 'list_field': { + 'type': 'list', + 'schema': { + 'type': 'media' + } + } + } + }) + + data = MultiDict([('list_field', + (BytesIO(b'file_content1'), 'test1.txt')), + ('list_field', + (BytesIO(b'file_content2'), 'test2.txt'))]) + resp = self.test_client.post('/test_res/', data=data, + content_type='multipart/form-data') + r, status = self.parse_response(resp) + self.assert201(status) + + _db = self.connection[MONGO_DBNAME] + id_field = self.domain['test_res']['id_field'] + obj = _db.test_res.find_one({id_field: ObjectId(r[id_field])}) + media_ids = obj['list_field'] + self.assertEqual(len(media_ids), 2) + with self.app.test_request_context(): + for i in [0, 1]: + self.assertTrue(self.app.media.exists(media_ids[i], 'test_res')) + + r, status = self.parse_response( + self.test_client.get('/test_res/%s' % r[id_field])) + files = r['list_field'] + self.assertEqual(b64decode(files[0]), b'file_content1') + self.assertEqual(b64decode(files[1]), b'file_content2') + def test_post_referential_integrity(self): data = {"person": self.unknown_item_id} r, status = self.post('/invoices/', data=data) From 206434eeb5441bc6535346dbfe444e223c819445 Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Fri, 4 Nov 2016 12:03:44 +0100 Subject: [PATCH 068/821] Flake8 fixes --- eve/flaskapp.py | 4 ++-- eve/tests/methods/post.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 7f521755d..6313a2298 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -701,8 +701,8 @@ def _set_resource_projection(self, ds, schema, settings): settings['_media'] = [field for field, definition in schema.items() if definition.get('type') == 'media' or (definition.get('type') == 'list' and - definition.get('schema', {}).get('type') - == 'media')] + definition.get('schema', {}).get('type') == + 'media')] if settings['_media'] and not self.media: raise ConfigException('A media storage class of type ' diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 56c206631..618baf013 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -15,6 +15,7 @@ from werkzeug.datastructures import MultiDict + class TestPost(TestBase): def test_unknown_resource(self): _, status = self.post(self.unknown_resource_url, data={}) @@ -265,7 +266,7 @@ def test_post_auto_collapse_multiple_keys(self): self.assert201(status) resp = self.test_client.post('/test_res/', data=data, - content_type='multipart/form-data') + content_type='multipart/form-data') r, status = self.parse_response(resp) self.assert201(status) @@ -298,7 +299,8 @@ def test_post_auto_collapse_media_list(self): self.assertEqual(len(media_ids), 2) with self.app.test_request_context(): for i in [0, 1]: - self.assertTrue(self.app.media.exists(media_ids[i], 'test_res')) + self.assertTrue( + self.app.media.exists(media_ids[i], 'test_res')) r, status = self.parse_response( self.test_client.get('/test_res/%s' % r[id_field])) From 33e7605bffdc6f47d6b22ea573578c540af9db24 Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Sun, 6 Nov 2016 13:31:56 +0100 Subject: [PATCH 069/821] Added config setting AUTO_CREATE_LISTS If set to True, fields, which expect type list, but receive another type, will convert that type to a list with one element. --- eve/default_settings.py | 1 + eve/methods/common.py | 4 ++++ eve/tests/methods/post.py | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/eve/default_settings.py b/eve/default_settings.py index b78aab929..2f524bcfb 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -203,6 +203,7 @@ MULTIPART_FORM_FIELDS_AS_JSON = False AUTO_COLLAPSE_MULTI_KEYS = False +AUTO_CREATE_LISTS = False SCHEMA_ENDPOINT = None diff --git a/eve/methods/common.py b/eve/methods/common.py index 043e4ab58..fd087e937 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -387,6 +387,10 @@ def serialize(document, resource=None, schema=None, fields=None): for opttype in field_schema.get(x_of_type, []): schema = {field: {'type': opttype}} serialize(document, schema=schema) + if config.AUTO_CREATE_LISTS and field_type == 'list': + # Convert single values to lists + if not isinstance(document[field], list): + document[field] = [document[field]] if 'schema' in field_schema: field_schema = field_schema['schema'] if 'dict' in (field_type, field_schema.get('type')): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 618baf013..9717ca25a 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -308,6 +308,26 @@ def test_post_auto_collapse_media_list(self): self.assertEqual(b64decode(files[0]), b'file_content1') self.assertEqual(b64decode(files[1]), b'file_content2') + def test_post_auto_create_lists(self): + self.app.config['AUTO_CREATE_LISTS'] = True + self.app.register_resource('test_res', { + 'schema': { + 'list_field': { + 'type': 'list', + 'schema': { + 'type': 'string' + } + } + } + }) + + data = MultiDict([("list_field", "value1")]) + resp = self.test_client.post( + '/test_res/', data=data, + content_type='application/x-www-form-urlencoded') + r, status = self.parse_response(resp) + self.assert201(status) + def test_post_referential_integrity(self): data = {"person": self.unknown_item_id} r, status = self.post('/invoices/', data=data) From 710e76373e99760304c79bf4ed20ff1c05a321e0 Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Sun, 6 Nov 2016 21:38:00 +0100 Subject: [PATCH 070/821] Added missing list conversion for lists of media --- eve/methods/delete.py | 7 ++++++- eve/tests/methods/post.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 268ea8f7e..1341d021c 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -142,7 +142,12 @@ def deleteitem_internal( for field in media_fields: if field in original: - app.media.delete(original[field], resource) + media_field = original[field] + if isinstance(media_field, list): + for file_id in media_field: + app.media.delete(file_id, resource) + else: + app.media.delete(original[field], resource) id = original[resource_def['id_field']] app.data.remove(resource, {resource_def['id_field']: id}) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 9717ca25a..8d8b6face 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -283,6 +283,7 @@ def test_post_auto_collapse_media_list(self): } }) + # Create a document data = MultiDict([('list_field', (BytesIO(b'file_content1'), 'test1.txt')), ('list_field', @@ -292,6 +293,7 @@ def test_post_auto_collapse_media_list(self): r, status = self.parse_response(resp) self.assert201(status) + # check that the files were created _db = self.connection[MONGO_DBNAME] id_field = self.domain['test_res']['id_field'] obj = _db.test_res.find_one({id_field: ObjectId(r[id_field])}) @@ -302,12 +304,25 @@ def test_post_auto_collapse_media_list(self): self.assertTrue( self.app.media.exists(media_ids[i], 'test_res')) + # GET the document and check the file content is correct r, status = self.parse_response( self.test_client.get('/test_res/%s' % r[id_field])) files = r['list_field'] self.assertEqual(b64decode(files[0]), b'file_content1') self.assertEqual(b64decode(files[1]), b'file_content2') + # DELETE the document + resp = self.test_client.delete('/test_res/%s' % r['_id'], + headers={'If-Match': r['_etag']}) + r, status = self.parse_response(resp) + self.assert204(status) + + # Check files were deleted + with self.app.test_request_context(): + for i in [0, 1]: + self.assertFalse( + self.app.media.exists(media_ids[i], 'test_res')) + def test_post_auto_create_lists(self): self.app.config['AUTO_CREATE_LISTS'] = True self.app.register_resource('test_res', { From 3cd548b321d885b83390831d291014f0a2b5d85b Mon Sep 17 00:00:00 2001 From: Conrad Burchert Date: Tue, 15 Nov 2016 14:27:31 +0100 Subject: [PATCH 071/821] Added documentation for AUTO_COLLAPSE_MULTI_KEYS and AUTO_CREATE_LISTS --- docs/config.rst | 20 ++++++++++++++++++++ docs/features.rst | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index eecff35d0..62945e4ec 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -610,6 +610,26 @@ uppercase. should be formatted at :ref:`multipart`. Defaults to ``False``. +``AUTO_COLLAPSE_MULTI_KEYS`` If set to ``True``, multiple values sent + with the same key, submitted using the + ``application/x-www-form-urlencoded`` or + ``multipart/form-data`` content types, + will automatically be converted to a list of + values. + + When using this together with + ``AUTO_CREATE_LISTS`` it becomes possible + to use lists of media fields. + + Defaults to ``False`` + +``AUTO_CREATE_LISTS`` When submitting a non ``list`` type value + for a field with type ``list``, + automatically create a one element list + before running the validators. + + Defaults to ``False`` + ``OPLOG`` Set it to ``True`` to enable the :ref:`oplog`. Defaults to ``False``. diff --git a/docs/features.rst b/docs/features.rst index c3c7a11cb..9b6077a32 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1656,6 +1656,15 @@ quotes). If ever in doubt if what you are submitting is a valid JSON string you can try passing it from the JSON Validator at http://jsonlint.com/ to be sure that it is correct. +.. _media_lists: + +Using lists of media +~~~~~~~~~~~~~~~~~~~~ +When using lists of media, there is no way to submit these in the default +configuration. Enable ``AUTO_COLLAPSE_MULTI_KEYS`` and ``AUTO_CREATE_LISTS`` +to make this possible. This allows to send multiple values for one key in +``multipart/form-data`` requests and in this way upload a list of files. + .. _geojson_feature: GeoJSON From ff36cb6cd7a08c0b475309905d82877e542dda31 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Nov 2016 09:31:02 +0100 Subject: [PATCH 072/821] flake8 --- eve/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eve/utils.py b/eve/utils.py index 3065a7b4f..51efd5919 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -445,5 +445,6 @@ def auto_fields(resource): return fields + # Base string type that is compatible with both Python 2.x and 3.x. str_type = str if sys.version_info[0] == 3 else basestring From 9c813ba687dc0ebd19df6bd03100cfecab5d104c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Nov 2016 09:37:38 +0100 Subject: [PATCH 073/821] Changelog for #933 --- CHANGES | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES b/CHANGES index b61ba3f34..a27ba086a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,16 @@ In Development Version 0.7 ~~~~~~~~~~~ +- New: ``AUTO_COLLAPSE_MULTI_KEYS``. If set to ``True``, multiple values sent + with the same key, submitted using the ``application/x-www-form-urlencoded`` + or ``multipart/form-data`` content types, will automatically be converted to + a list of values. When using this together with ``AUTO_CREATE_LISTS`` it + becomes possible to use lists of media fields. Defaults to ``False``. Closes + #932 (Conrad Burchert). + +- New: ``AUTO_CREATE_LISTS``. When submitting a non ``list`` type value for + a field with type ``list``, automatically create a one element list before + running the validators. Defaults to ``False`` (Conrad Burchert). - New: Flask-PyMongo compatibility for for ``MONGO_CONNECT`` config setting (Massimo Scamarcia). From ab2a2274d39143e39453a29fe55f4fa1c23c0bb9 Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 18:35:31 +0100 Subject: [PATCH 074/821] Provide: Optional pretty printing of GET responses --- eve/render.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/eve/render.py b/eve/render.py index 1f5a403a9..53948261e 100644 --- a/eve/render.py +++ b/eve/render.py @@ -275,7 +275,12 @@ def render_json(data): .. versionchanged:: 0.1.0 Support for optional HATEOAS. """ - return json.dumps(data, cls=app.data.json_encoder_class, + set_indent = None + + # make pretty prints available + if 'GET' in request.method and 'pretty' in request.args: + set_indent = 4 + return json.dumps(data, indent=set_indent, cls=app.data.json_encoder_class, sort_keys=config.JSON_SORT_KEYS) @@ -377,7 +382,7 @@ def xml_add_links(data): for rel, link in ordered_links.items(): if isinstance(link, list): xml += ''.join([chunk % (rel, utils.escape(d['href']), - utils.escape(d['title'])) for d in link]) + utils.escape(d['title'])) for d in link]) else: xml += ''.join(chunk % (rel, utils.escape(link['href']), link['title'])) From d0f380791c43321dfbface9aff79186f8b3f27aa Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 18:36:28 +0100 Subject: [PATCH 075/821] Provide: test for pretty printed GET response --- eve/tests/response.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/eve/tests/response.py b/eve/tests/response.py index b6c978a70..2b2fa1893 100644 --- a/eve/tests/response.py +++ b/eve/tests/response.py @@ -33,6 +33,15 @@ def test_response_object(self): meta = response.get('_meta') self.assertTrue(isinstance(meta, dict)) + def test_response_pretty(self): + # check if pretty printing was successful by checking the length of the response + # since pretty printing the respone makes it longer and not type dict + # anymore + self.r = self.test_client.get('/%s/?pretty' % self.empty_resource) + response = self.r.get_data().decode() + self.assertEqual(len(response), 300) + self.assertTrue(isinstance(response, unicode)) + class TestNoHateoas(TestBase): From 38bc887922b280c8a4ac260433e7ba7e78b90422 Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 18:36:59 +0100 Subject: [PATCH 076/821] Provide: Documentation on how to use pretty prints --- docs/features.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/features.rst b/docs/features.rst index c3c7a11cb..7a60fda9c 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -298,6 +298,16 @@ You also have the option to validate the incoming filters against the resource's schema and refuse to apply the filtering if any filters are invalid, by using the ``VALIDATE_FILTERING`` system setting (see :ref:`global`) +Pretty Printing +--------------- +You can pretty print the response by specifying a query parameter named `pretty`: + +.. code-block:: console + + $ curl -i http://eve-demo.herokuapp.com/people?pretty + HTTP/1.1 200 OK + +Now the response payload will have indentations. Sorting ------- From 27106b68aa77b88e24101d65154a04aa24dab864 Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 18:37:50 +0100 Subject: [PATCH 077/821] Gitignore virtualenv folder and docs subfolders --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 9f55cefe9..a2ee0ea08 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /.pydevproject /.settings .ropeproject +docs/html +docs/doctrees # Eve run.py @@ -9,6 +11,8 @@ settings.py # Python *.py[co] +.venv/ +venv/ # Gedit *~ From 34b093b77b3886c3d8b7176e2e928e9206187914 Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 18:38:20 +0100 Subject: [PATCH 078/821] Change AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cf3733c7c..b41968870 100644 --- a/AUTHORS +++ b/AUTHORS @@ -52,6 +52,7 @@ Patches and Contributions - Hamdy - Hannes Tiede - Harro van der Klauw +- Hasan Pekdemir - Henrique Barroso - James Stewart - Jaroslav Semančík From ee00c868d3e2077c15e4e8eac301b1fc7bd5f1ea Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 19:10:09 +0100 Subject: [PATCH 079/821] Fix failed Travis CI builds - Make sure your code conforms to PEP8 - Check changes against multiple python versions --- eve/tests/response.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eve/tests/response.py b/eve/tests/response.py index 2b2fa1893..00ab09090 100644 --- a/eve/tests/response.py +++ b/eve/tests/response.py @@ -34,13 +34,14 @@ def test_response_object(self): self.assertTrue(isinstance(meta, dict)) def test_response_pretty(self): - # check if pretty printing was successful by checking the length of the response - # since pretty printing the respone makes it longer and not type dict - # anymore + # check if pretty printing was successful by checking the length of the + # response since pretty printing the respone makes it longer and not + # type dict anymore self.r = self.test_client.get('/%s/?pretty' % self.empty_resource) response = self.r.get_data().decode() self.assertEqual(len(response), 300) - self.assertTrue(isinstance(response, unicode)) + # python2 and python3 compatible (check for unicode or str) + self.assertTrue(isinstance(response, basestring)) class TestNoHateoas(TestBase): From 034d25f22253d19a8020b7d88a93a1305c57c1da Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Sun, 4 Dec 2016 20:09:07 +0100 Subject: [PATCH 080/821] Fix pretty print response test --- eve/tests/response.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/eve/tests/response.py b/eve/tests/response.py index 00ab09090..a4709b443 100644 --- a/eve/tests/response.py +++ b/eve/tests/response.py @@ -40,8 +40,6 @@ def test_response_pretty(self): self.r = self.test_client.get('/%s/?pretty' % self.empty_resource) response = self.r.get_data().decode() self.assertEqual(len(response), 300) - # python2 and python3 compatible (check for unicode or str) - self.assertTrue(isinstance(response, basestring)) class TestNoHateoas(TestBase): From 421442f0b7934e9cb0ea8084d5d58861adc58c8b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 5 Dec 2016 08:07:56 +0100 Subject: [PATCH 081/821] Revert "Gitignore virtualenv folder and docs subfolders" This reverts commit 27106b68aa77b88e24101d65154a04aa24dab864. --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index a2ee0ea08..9f55cefe9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ /.pydevproject /.settings .ropeproject -docs/html -docs/doctrees # Eve run.py @@ -11,8 +9,6 @@ settings.py # Python *.py[co] -.venv/ -venv/ # Gedit *~ From 4e67b33ff9c66649e5e9721a704c4fe12288746f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 5 Dec 2016 08:10:09 +0100 Subject: [PATCH 082/821] Changelog update for #946 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index a27ba086a..cf94637b7 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ In Development Version 0.7 ~~~~~~~~~~~ +- New: Pretty printing.You can pretty print the response by specifying a query + parameter named ``?pretty`` (Hasan Pekdemir). + - New: ``AUTO_COLLAPSE_MULTI_KEYS``. If set to ``True``, multiple values sent with the same key, submitted using the ``application/x-www-form-urlencoded`` or ``multipart/form-data`` content types, will automatically be converted to From f9b712c6d23a475b757d93c591c82d27a9369e75 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 5 Dec 2016 08:18:18 +0100 Subject: [PATCH 083/821] Improve pretty printing example in the docs --- docs/features.rst | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index d0111e770..abafd5a69 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -300,14 +300,42 @@ schema and refuse to apply the filtering if any filters are invalid, by using th Pretty Printing --------------- -You can pretty print the response by specifying a query parameter named `pretty`: +You can pretty print the response by specifying a query parameter named +``pretty``: .. code-block:: console $ curl -i http://eve-demo.herokuapp.com/people?pretty HTTP/1.1 200 OK -Now the response payload will have indentations. + { + "_items": [ + { + "_updated": "Tue, 19 Apr 2016 08:19:00 GMT", + "firstname": "John", + "lastname": "Doe", + "born": "Thu, 27 Aug 1970 14:37:13 GMT", + "role": [ + "author" + ], + "location": { + "city": "Auburn", + "address": "422 South Gay Street" + }, + "_links": { + "self": { + "href": "people/5715e9f438345b3510d27eb8", + "title": "person" + } + }, + "_created": "Tue, 19 Apr 2016 08:19:00 GMT", + "_id": "5715e9f438345b3510d27eb8", + "_etag": "86dc6b45fe7e2f41f1ca53a0e8fda81224229799" + }, + ... + ] + } + Sorting ------- From 73fbd1b6f8b87ca2cfb13c86a4911622d9ea900e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 8 Dec 2016 16:30:10 +0100 Subject: [PATCH 084/821] Upgrade to Flask 0.11.1 Flask 0.11.1 crashes when using the obsolete app.error_handler_spec() method. Using app_register_error_handler() instead. See https://github.com/pallets/flask/issues/1837 for details. Closes #904. Closes #945. --- CHANGES | 1 + eve/flaskapp.py | 7 ++++++- requirements.txt | 6 +++--- setup.py | 8 ++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index cf94637b7..2de77b756 100644 --- a/CHANGES +++ b/CHANGES @@ -107,6 +107,7 @@ Version 0.7 Version 0.6.5 ~~~~~~~~~~~~~ +- Flask 0.11.1 is now supported. Closes #945 and #904. - Fix: Deprecation warning from Flask. Closes #898 (George Lestaris). - Fix: add Support serialization on lists using anyof, oneof, allof, noneof. Closes #876 (Carles Bruguera). diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 6313a2298..4f28033ff 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -918,10 +918,15 @@ def register_error_handlers(self): """ Register custom error handlers so we make sure that all errors return a parseable body. + .. versionchanged: 0.6.5 + Replace obsolete app.register_error_handler_spec() with + register_error_handler(), which works with Flask>=0.11.1. Closes + #904, #945. + .. versionadded:: 0.4 """ for code in self.config['STANDARD_ERRORS']: - self.error_handler_spec[None][code] = error_endpoint + self.register_error_handler(code, error_endpoint) def _init_oplog(self): """ If enabled, configures the OPLOG endpoint. diff --git a/requirements.txt b/requirements.txt index a11ca110c..503e3a34a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ Cerberus==0.9.2 Events==0.2.1 Flask-PyMongo==0.4.1 -Flask==0.10.1 +Flask==0.11.1 itsdangerous==0.24 -Jinja2==2.7.3 +Jinja2==2.8 MarkupSafe==0.23 pymongo==3.2.1 simplejson==3.8.2 -Werkzeug==0.11.4 +Werkzeug==0.11.11 diff --git a/setup.py b/setup.py index 85053479c..a02b5e3db 100755 --- a/setup.py +++ b/setup.py @@ -9,11 +9,11 @@ 'cerberus>=0.9.2,<0.10', 'events>=0.2.1,<0.3', 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<0.11.4', + 'werkzeug>=0.9.4,<0.11.11', 'markupsafe>=0.23,<1.0', - 'jinja2>=2.7.2,<3.0', - 'itsdangerous>=0.22,<1.0', - 'flask>=0.10.1,<0.11', + 'jinja2>=2.8,<3.0', + 'itsdangerous>=0.24,<1.0', + 'flask>=0.10.1,<=0.11.1', 'pymongo>=3.2', 'flask-pymongo>=0.4', ] From 87d2b2596c15074be67d1b772e48131b97b2ad12 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 8 Dec 2016 16:39:04 +0100 Subject: [PATCH 085/821] Dump v0.6.5 let's go straight for 0.7 instead --- CHANGES | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 2de77b756..8c084928d 100644 --- a/CHANGES +++ b/CHANGES @@ -98,28 +98,26 @@ Version 0.7 - Fix: ETag request headers which conform to RFC 7232/2.3 (double quoted value) are now properly processed. Addresses #794. -- Docs: remove the deprecated ``--ditribute`` virtualenv option (Eugene - Prikazchikov). - -- Docs: add date and subdocument fields filtering examples. Closes #924. - - -Version 0.6.5 -~~~~~~~~~~~~~ - -- Flask 0.11.1 is now supported. Closes #945 and #904. - Fix: Deprecation warning from Flask. Closes #898 (George Lestaris). + - Fix: add Support serialization on lists using anyof, oneof, allof, noneof. Closes #876 (Carles Bruguera). + - Fix: update security example snippets to match with current API (Stanislav Filin). + - Fix: ``notifications.py`` example snippet crashes due to lack of ``DOMAIN`` setting (Stanislav Filin). +- Docs: remove the deprecated ``--ditribute`` virtualenv option (Eugene + Prikazchikov). +- Docs: add date and subdocument fields filtering examples. Closes #924. - Docs: add Eve-Neo4j to the extensions page (Rodrigo Rodriguez). - Docs: stress that alternate backends are supported via community extensions. - Docs: clarify that Redis is an optional dependency (Mateusz Łoskot). +- Update: Flask 0.11.1 is now supported. Closes #945 and #904. + Stable ------ From 298d1bd42a97973b9b766282d5d06839e6f84300 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 8 Dec 2016 17:23:59 +0100 Subject: [PATCH 086/821] Bump version to 0.7.dev0 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index ce3b19221..80e07120b 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.6.5.dev0' +__version__ = '0.7.dev0' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index a02b5e3db..47874279a 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.6.5.dev0', + version='0.7.dev0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 3f5e51467f75c6636b1b56ec58763a78843f95bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 9 Dec 2016 09:44:54 +0100 Subject: [PATCH 087/821] Location header support. Closes #795. --- CHANGES | 4 ++++ docs/features.rst | 16 ++++++++++++---- eve/methods/post.py | 15 +++++++++++---- eve/tests/methods/post.py | 28 ++++++++++++++++++++++++---- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/CHANGES b/CHANGES index 8c084928d..937b8eb32 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ In Development Version 0.7 ~~~~~~~~~~~ +- New: ``Location`` header is returned on ``201 Created`` POST responses. If + will contain the URI to the created document. If bulk inserts are enabled, + only the first document URI is returned. Closes #795. + - New: Pretty printing.You can pretty print the response by specifying a query parameter named ``?pretty`` (Hasan Pekdemir). diff --git a/docs/features.rst b/docs/features.rst index abafd5a69..779df3d36 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -592,7 +592,11 @@ metadata: "_links": {"self": {"href": "people/50ae43339fa12500024def5b", "title": "person"}} } -However, in order to reduce the number of loopbacks, a client might also submit +When a ``201 Created`` is returned following a POST request, the ``Location`` +header is also included with the response. Its value is the URI to the new +document. + +In order to reduce the number of loopbacks, a client might also submit multiple documents with a single request. All it needs to do is enclose the documents in a JSON list: @@ -626,9 +630,13 @@ The response will be a list itself, with the state of each document: } When multiple documents are submitted the API takes advantage of MongoDB *bulk -insert* capabilities which means that not only there's just one single request -traveling from the client to the remote API, but also that only one loopback is -performed between the API server and the database. +insert* capabilities which means that not only there's just one request +traveling from the client to the remote API, but also that a single loopback is +performed between the API server and the database. + +In case of successful multiple inserts, keep in mind that the ``Location`` +header only returns the URI of the first created document. + Data Validation --------------- diff --git a/eve/methods/post.py b/eve/methods/post.py index 7e2503299..5846866fe 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -20,7 +20,7 @@ from eve.methods.common import parse, payload, ratelimit, \ pre_event, store_media_files, resolve_user_restricted_access, \ resolve_embedded_fields, build_response_document, marshal_write_response, \ - resolve_sub_resource_path, resolve_document_etag, oplog_push + resolve_sub_resource_path, resolve_document_etag, oplog_push, resource_link from eve.versioning import resolve_document_version, \ insert_versioning_documents @@ -64,6 +64,9 @@ def post_internal(resource, payl=None, skip_validation=False): discussion, and a typical use case. :param skip_validation: skip payload validation before write (bool) + .. versionchanged:: 0.7 + Add support for Location header. Closes #795. + .. versionchanged:: 0.6 Fix: since v0.6, skip_validation = True causes a 422 response (#726). @@ -152,6 +155,7 @@ def post_internal(resource, payl=None, skip_validation=False): documents = [] results = [] failures = 0 + id_field = resource_def['id_field'] if config.BANDWIDTH_SAVER is True: embedded_fields = [] @@ -253,8 +257,8 @@ def post_internal(resource, payl=None, skip_validation=False): for document in documents: # either return the custom ID_FIELD or the id returned by # data.insert(). - document[resource_def['id_field']] = \ - document.get(resource_def['id_field'], ids.pop(0)) + id_ = document.get(id_field, ids.pop(0)) + document[id_field] = id_ # build the full response document result = document @@ -294,4 +298,7 @@ def post_internal(resource, payl=None, skip_validation=False): % failures, } - return response, None, None, return_code + location_header = None if return_code != 201 or not documents else \ + [('Location', '%s/%s' % (resource_link(), documents[0][id_field]))] + + return response, None, None, return_code, location_header diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 8d8b6face..d34e3fc02 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -633,7 +633,7 @@ def test_post_bandwidth_saver(self): def test_post_alternative_payload(self): payl = {"ref": "5432112345678901234567890", "role": ["agent"]} with self.app.test_request_context(self.known_resource_url): - r, _, _, status = post(self.known_resource, payl=payl) + r, _, _, status, _ = post(self.known_resource, payl=payl) self.assert201(status) self.assertPostResponse(r) @@ -803,7 +803,8 @@ def test_post_internal(self): test_value = "1234567890123456789054321" payload = {test_field: test_value} with self.app.test_request_context(self.known_resource_url): - r, _, _, status = post_internal(self.known_resource, payl=payload) + r, _, _, status, _ = post_internal(self.known_resource, + payl=payload) self.assert201(status) def test_post_internal_skip_validation(self): @@ -813,8 +814,9 @@ def test_post_internal_skip_validation(self): test_value = "1234567890123456789054321" payload = {test_field: test_value} with self.app.test_request_context(self.known_resource_url): - r, _, _, status = post_internal(self.known_resource, payl=payload, - skip_validation=True) + r, _, _, status, _ = post_internal(self.known_resource, + payl=payload, + skip_validation=True) self.assert201(status) def test_post_nested(self): @@ -855,6 +857,24 @@ def test_post_type_coercion(self): data = {'ref': '1234567890123456789054321', 'aninteger': '42.3'} self.assertPostItem(data, 'aninteger', 42) + def test_post_location_header_hateoas_on(self): + self.app.config['HATEOAS'] = True + data = json.dumps({'ref': '1234567890123456789054321'}) + headers = [('Content-Type', 'application/json')] + r = self.test_client.post(self.known_resource_url, data=data, + headers=headers) + self.assertTrue('Location' in r.headers) + self.assertTrue(self.known_resource_url in r.headers['Location']) + + def test_post_location_header_hateoas_off(self): + self.app.config['HATEOAS'] = False + data = json.dumps({'ref': '1234567890123456789054321'}) + headers = [('Content-Type', 'application/json')] + r = self.test_client.post(self.known_resource_url, data=data, + headers=headers) + self.assertTrue('Location' in r.headers) + self.assertTrue(self.known_resource_url in r.headers['Location']) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From 42be190b924be5fb50e6b496a7a4832f85d1167a Mon Sep 17 00:00:00 2001 From: NotSpecial Date: Sun, 11 Dec 2016 02:08:44 +0100 Subject: [PATCH 088/821] Added serialization rule for boolean --- eve/io/mongo/mongo.py | 2 ++ eve/tests/methods/common.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 88f565f1b..2c8a4bc25 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -80,6 +80,8 @@ class Mongo(DataLayer): 'integer': lambda value: int(value) if value is not None else None, 'float': lambda value: float(value) if value is not None else None, 'number': lambda val: json.loads(val) if val is not None else None, + 'boolean': lambda v: + {'1': True, 'true': True, '0': False, 'false': False}[str(v).lower()], 'dbref': lambda value: DBRef(value['$col'], value['$id'], value['$db'] if '$db' in value else None) if value is not None else None, diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 3e66d4ec5..51b3084ea 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -305,6 +305,15 @@ def test_serialize_number(self): isinstance(serialized['anumber'], expected_type) ) + def test_serialize_boolean(self): + schema = {'bool': {'type': 'boolean'}} + + with self.app.app_context(): + for val in [1, '1', 0, '0', 'true', 'True', 'false', 'False']: + doc = {'bool': val} + serialized = serialize(doc, schema=schema) + self.assertTrue(isinstance(serialized['bool'], bool)) + def test_serialize_inside_x_of_rules(self): for x_of in ['allof', 'anyof', 'oneof', 'noneof']: schema = { From 38935d6b100543e7594505696c4ad6f549eca770 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 16 Dec 2016 10:31:31 +0100 Subject: [PATCH 089/821] Changelog for #948 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 937b8eb32..c4e097950 100644 --- a/CHANGES +++ b/CHANGES @@ -91,6 +91,8 @@ Version 0.7 - Change: ETag response header now conforms to RFC 7232/2.3 and is surrounded by double quotes. Closes #794. +- Fix: improve serialization of boolean values. Closes #947 (NotSpecial). + - Fix: fix intermittently failing test. Closes #934 (Conrad Burchert). - Fix: Multiple, fast (within a 1 second window) and neutral (no actual changes) From 5e43ccbe0a6582b893f3a150819bb4288cab7f1b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 16 Dec 2016 10:31:55 +0100 Subject: [PATCH 090/821] NotSpecial --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b41968870..815eccfe0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -102,6 +102,7 @@ Patches and Contributions - Nick Park - Nicolas Bazire - Nicolas Carlier +- NotSpecial - Olivier Carrère - Olivier Poitrey - Ondrej Slinták From d0b26b19fc7c645a51f5ce680bf0d697395eca2b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 8 Dec 2016 11:03:34 +0100 Subject: [PATCH 091/821] Add OPTIMIZE_PAGINATION_FOR_SPEED Closes #944. Closes #853. Addresses #883. --- CHANGES | 11 ++++++++++ docs/config.rst | 15 ++++++++++++++ eve/default_settings.py | 2 ++ eve/methods/get.py | 44 ++++++++++++++++++++++------------------ eve/tests/methods/get.py | 14 +++++++++++++ 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/CHANGES b/CHANGES index c4e097950..800510193 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,17 @@ In Development Version 0.7 ~~~~~~~~~~~ +- New: ``OPTIMIZE_PAGINATION_FOR_SPEED``. Set this to ``True`` to improve + pagination performance. When optimization is active no count operation, which + can be slow on large collections, is performed on the database. This does + have a few consequences. Firstly, no document count is returned. Secondly, + ``HATEOAS`` is less accurate: no last page link is available, and next page + link is always included, even on last page. On big collections, switching + this feature on can greatly improve performance. Defaults to ``False`` + (slower performance; document count included; accurate ``HATEOAS``). Closes + #944 and #853. + + - New: ``Location`` header is returned on ``201 Created`` POST responses. If will contain the URI to the created document. If bulk inserts are enabled, only the first document URI is returned. Closes #795. diff --git a/docs/config.rst b/docs/config.rst index 62945e4ec..94e367dad 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -140,6 +140,21 @@ uppercase. ``PAGINATION_DEFAULT`` Default value for QUERY_MAX_RESULTS. Defaults to 25. +``OPTMIMIZE_PAGINATION_FOR_SPEED`` Set this to ``True`` to improve pagination + performance. When optimization is active no + count operation, which can be slow on large + collections, is performed on the database. + This does have a few consequences. + Firstly, no document count is returned. + Secondly, ``HATEOAS`` is less accurate: no + last page link is available, and next page + link is always included, even on last page. + On big collections, switching this feature + on can greatly improve performance. + Defaults to ``False`` (slower performance; + document count included; accurate + ``HATEOAS``). + ``QUERY_WHERE`` Key for the filters query parameter. Defaults to ``where``. ``QUERY_SORT`` Key for the sort query parameter. Defaults to ``sort``. diff --git a/eve/default_settings.py b/eve/default_settings.py index 2f524bcfb..ebd799dfa 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -12,6 +12,7 @@ :license: BSD, see LICENSE for more details. .. versionchanged:: 0.7 + 'OPTIMIZE_PAGINATION_FOR_SPEED' added and set to False. 'OPLOG_RETURN_EXTRA_FIELD' added and set to False. 'ENFORCE_IF_MATCH'added and set to True. @@ -223,6 +224,7 @@ QUERY_AGGREGATION = 'aggregate' HEADER_TOTAL_COUNT = 'X-Total-Count' +OPTIMIZE_PAGINATION_FOR_SPEED = False # user-restricted resource access is disabled by default. AUTH_FIELD = None diff --git a/eve/methods/get.py b/eve/methods/get.py index 8ab242a84..aa0208462 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -197,8 +197,12 @@ def _perform_find(resource, lookup): last_modified = last_update if last_update > epoch() else None response[config.ITEMS] = documents - count = cursor.count(with_limit_and_skip=False) - headers.append((config.HEADER_TOTAL_COUNT, count)) + + if config.OPTIMIZE_PAGINATION_FOR_SPEED: + count = None + else: + count = cursor.count(with_limit_and_skip=False) + headers.append((config.HEADER_TOTAL_COUNT, count)) if config.DOMAIN[resource]['hateoas']: response[config.LINKS] = _pagination_links(resource, req, count) @@ -454,7 +458,7 @@ def getitem_internal(resource, **lookup): return response, last_modified, etag, 200 -def _pagination_links(resource, req, documents_count, document_id=None): +def _pagination_links(resource, req, document_count, document_id=None): """ Returns the appropriate set of resource links depending on the current page and the total number of documents returned by the query. @@ -516,32 +520,30 @@ def _pagination_links(resource, req, documents_count, document_id=None): % _links['parent']['href']} # modify the self link to add query params or version number - if documents_count: + if document_count: _links['self']['href'] = '%s%s' % (_links['self']['href'], q) - elif not documents_count and version and version not in ('all', 'diffs'): + elif not document_count and version and version not in ('all', 'diffs'): _links['self'] = document_link(resource, document_id, version) # create pagination links - if documents_count and config.DOMAIN[resource]['pagination']: + if config.DOMAIN[resource]['pagination']: # strip any queries from the self link if present _pagination_link = _links['self']['href'].split('?')[0] - if req.page * req.max_results < documents_count: + + if (req.page * req.max_results < document_count or + config.OPTIMIZE_PAGINATION_FOR_SPEED): q = querydef(req.max_results, req.where, req.sort, version, req.page + 1, other_params) _links['next'] = {'title': 'next page', 'href': '%s%s' % (_pagination_link, q)} - # in python 2.x dividing 2 ints produces an int and that's rounded - # before the ceil call. Have to cast one value to float to get - # a correct result. Wonder if 2 casts + ceil() call are actually - # faster than documents_count // req.max_results and then adding - # 1 if the modulo is non-zero... - last_page = int(math.ceil(documents_count / - float(req.max_results))) - q = querydef(req.max_results, req.where, req.sort, version, - last_page, other_params) - _links['last'] = {'title': 'last page', 'href': '%s%s' - % (_pagination_link, q)} + if document_count: + last_page = int(math.ceil(document_count / float( + req.max_results))) + q = querydef(req.max_results, req.where, req.sort, version, + last_page, other_params) + _links['last'] = {'title': 'last page', 'href': '%s%s' % ( + _pagination_link, q)} if req.page > 1: q = querydef(req.max_results, req.where, req.sort, version, @@ -572,8 +574,10 @@ def _meta_links(req, count): .. versionadded:: 0.5 """ - return { + meta = { config.QUERY_PAGE: req.page, config.QUERY_MAX_RESULTS: req.max_results, - 'total': count } + if config.OPTIMIZE_PAGINATION_FOR_SPEED is False: + meta['total'] = count + return meta diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 566ce43b0..bc88b82b5 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -73,6 +73,20 @@ def test_get_page(self): self.assert200(status) self.assertPage(response, status) + def test_get_perform_count_on_pagination_disabled(self): + self.app.config['OPTIMIZE_PAGINATION_FOR_SPEED'] = True + + r = self.test_client.get('%s?page=2' % self.known_resource_url) + self.assert200(r.status_code) + + body = json.loads(r.get_data()) + links = body['_links'] + self.assertFalse('last' in links) + self.assertFalse('total' in body['_meta']) + self.assertNextLink(links, 3) + self.assertPrevLink(links, 1) + self.assertFalse(self.app.config['HEADER_TOTAL_COUNT'] in r.headers) + def test_get_internal_page(self): with self.app.test_request_context(self.known_resource_url): response, _, _, status, _ = get_internal(self.known_resource) From 37f6533bbcd68d57ce504de9055fb7b527e04f4e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 17 Dec 2016 14:17:42 +0100 Subject: [PATCH 092/821] Fix Python 3.x compatibility issue Introduced with OPTIMIZE_PAGINATION_FOR_SPEED. --- .cache/v/cache/lastfailed | 1 + eve/methods/get.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .cache/v/cache/lastfailed diff --git a/.cache/v/cache/lastfailed b/.cache/v/cache/lastfailed new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/eve/methods/get.py b/eve/methods/get.py index aa0208462..4856fa2ec 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -530,7 +530,7 @@ def _pagination_links(resource, req, document_count, document_id=None): # strip any queries from the self link if present _pagination_link = _links['self']['href'].split('?')[0] - if (req.page * req.max_results < document_count or + if (req.page * req.max_results < (document_count or 0) or config.OPTIMIZE_PAGINATION_FOR_SPEED): q = querydef(req.max_results, req.where, req.sort, version, req.page + 1, other_params) From 997161e5a8f11b7efa3f92f3df6cd781febfd030 Mon Sep 17 00:00:00 2001 From: Giorgos Margaritis Date: Mon, 19 Dec 2016 17:50:05 +0200 Subject: [PATCH 093/821] Current stable mongodb version is 3.4 Fix bug that occured when (trying to) modify the _id of object. Used to give a warning, now gives internal error because the mongodb version is not in the list above. --- eve/io/mongo/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 2c8a4bc25..05480fcd9 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -482,7 +482,7 @@ def _change_request(self, resource, id_, changes, original, replace=False): self.driver.db.client.server_info()['version'][:3] if ( (server_version == '2.4' and e.code in (13596, 10148)) or - (server_version in ('2.6', '3.0', '3.2') and + (server_version in ('2.6', '3.0', '3.2', '3.4') and e.code in (66, 16837)) ): # attempt to update an immutable field. this usually From ae85d1b557fc73ec22e586ff346c3eba20ade573 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 20 Dec 2016 08:28:03 +0100 Subject: [PATCH 094/821] Changelog for #951 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 800510193..5ecab5bcd 100644 --- a/CHANGES +++ b/CHANGES @@ -102,6 +102,9 @@ Version 0.7 - Change: ETag response header now conforms to RFC 7232/2.3 and is surrounded by double quotes. Closes #794. +- Fix: fix crash when attempting to modify a document ``_id`` on MongoDB 3.4 + (Giorgos Margaritis) + - Fix: improve serialization of boolean values. Closes #947 (NotSpecial). - Fix: fix intermittently failing test. Closes #934 (Conrad Burchert). From 81dc41a2ba1332d89ea478bf10f06ef25bc9d3ba Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 20 Dec 2016 08:28:22 +0100 Subject: [PATCH 095/821] Giorgos Margaritis --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 815eccfe0..5ade60f0a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,7 @@ Patches and Contributions - George Lestaris - Gianfranco Palumbo - Gino Zhang +- Giorgos Margaritis - Gonéri Le Bouder - Grisha K. - Guillaume Royer From fce8e630f9394a3a58163cb236d3f9fb17aee937 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 3 Jan 2017 10:47:19 +0100 Subject: [PATCH 096/821] Clarify documentation for custom validators Cerberus dependency is pinned to v0.9.2. Full upgrade to Cerberus 1.0+ is planned with v0.8. Closes #796. --- CHANGES | 3 +++ docs/validation.rst | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 5ecab5bcd..dfc47d476 100644 --- a/CHANGES +++ b/CHANGES @@ -129,6 +129,9 @@ Version 0.7 - Fix: ``notifications.py`` example snippet crashes due to lack of ``DOMAIN`` setting (Stanislav Filin). +- Docs: clarify documentation for custom validators: Cerberus dependency is + still pinned to version 0.9.2. Upgrade to Cerberus 1.0+ is planned with v0.8. + Closes #796. - Docs: remove the deprecated ``--ditribute`` virtualenv option (Eugene Prikazchikov). - Docs: add date and subdocument fields filtering examples. Closes #924. diff --git a/docs/validation.rst b/docs/validation.rst index 63ab222fd..46d615630 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -131,6 +131,10 @@ For more information on We have only scratched the surface of data validation. Please make sure to check the Cerberus_ documentation for a complete list of available validation rules and data types. + + Also note that Cerberus requirement is pinned to version 0.9.2, which still + supports the ``validate_update`` method used for ``PATCH`` requests. + Upgrade to Cerberus 1.0+ is scheduled for Eve version 0.8. .. _unknown: From f41249791a85ed219a883e10ff258f4067680459 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 4 Jan 2017 10:50:41 +0100 Subject: [PATCH 097/821] Add Python 3.6 as supported interpreter. Closes #954. --- .travis.yml | 2 ++ CHANGES | 2 ++ docs/index.rst | 3 ++- tox.ini | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 849f19801..f276c95af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: env: TOX_ENV=flake8 - python: 3.5 env: TOX_ENV=py35 + - python: 3.6-dev + env: TOX_ENV=py36 script: - tox -e $TOX_ENV services: diff --git a/CHANGES b/CHANGES index dfc47d476..20d18be80 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ In Development Version 0.7 ~~~~~~~~~~~ +- New: Add Python 3.6 as a supported interpreter. + - New: ``OPTIMIZE_PAGINATION_FOR_SPEED``. Set this to ``True`` to improve pagination performance. When optimization is active no count operation, which can be slow on large collections, is performed on the database. This does diff --git a/docs/index.rst b/docs/index.rst index 36df230d7..ba8866e48 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,8 @@ Eve is powered by Flask_, Cerberus_, Events_ and MongoDB_. Support for SQL-Alchemy, Elasticsearch and Neo4js as alternate backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5 and PyPy. +The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and +PyPy. Eve is Simple ------------- diff --git a/tox.ini b/tox.ini index e21c1225c..96ca9b1a4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py26,py27,py33,py34,py35,pypy,flake8 +envlist=py26,py27,py33,py34,py35,py36,pypy,flake8 [testenv] commands=python setup.py test {posargs} From aab9c6fc59f219bbc21a29abb676fe8997a53e38 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jan 2017 16:38:17 +0100 Subject: [PATCH 098/821] Update license to 2017. Closes #955. --- CHANGES | 1 + LICENSE | 2 +- eve/__init__.py | 2 +- eve/auth.py | 2 +- eve/default_settings.py | 2 +- eve/defaults.py | 2 +- eve/endpoints.py | 2 +- eve/exceptions.py | 2 +- eve/flaskapp.py | 2 +- eve/io/__init__.py | 2 +- eve/io/base.py | 2 +- eve/io/media.py | 2 +- eve/io/mongo/__init__.py | 2 +- eve/io/mongo/geo.py | 2 +- eve/io/mongo/media.py | 2 +- eve/io/mongo/mongo.py | 2 +- eve/io/mongo/parser.py | 2 +- eve/io/mongo/validation.py | 2 +- eve/methods/__init__.py | 2 +- eve/methods/common.py | 2 +- eve/methods/delete.py | 2 +- eve/methods/get.py | 2 +- eve/methods/patch.py | 2 +- eve/methods/post.py | 2 +- eve/methods/put.py | 2 +- eve/render.py | 2 +- eve/utils.py | 2 +- eve/validation.py | 2 +- 28 files changed, 28 insertions(+), 27 deletions(-) diff --git a/CHANGES b/CHANGES index 20d18be80..96b983933 100644 --- a/CHANGES +++ b/CHANGES @@ -141,6 +141,7 @@ Version 0.7 - Docs: stress that alternate backends are supported via community extensions. - Docs: clarify that Redis is an optional dependency (Mateusz Łoskot). +- Update license to 2017. Closes #955. - Update: Flask 0.11.1 is now supported. Closes #945 and #904. Stable diff --git a/LICENSE b/LICENSE index ac19726ff..afe9010b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016 by Nicola Iarocci and contributors. See AUTHORS +Copyright (c) 2017 by Nicola Iarocci and contributors. See AUTHORS for more details. Some rights reserved. diff --git a/eve/__init__.py b/eve/__init__.py index 80e07120b..5790633ea 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -6,7 +6,7 @@ An out-of-the-box REST Web API that's as dangerous as you want it to be. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. .. versionchanged:: 0.5 diff --git a/eve/auth.py b/eve/auth.py index c15691fd6..930b10a8e 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -6,7 +6,7 @@ Allow API endpoints to be secured via BasicAuth and derivates. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from flask import request, Response, current_app as app, g, abort diff --git a/eve/default_settings.py b/eve/default_settings.py index ebd799dfa..fd3222eeb 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -8,7 +8,7 @@ appropriately, by using a custom settings module (see the optional 'settings' argument or the EVE_SETTING environment variable). - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. .. versionchanged:: 0.7 diff --git a/eve/defaults.py b/eve/defaults.py index 1d59bec3a..f0c0d4651 100644 --- a/eve/defaults.py +++ b/eve/defaults.py @@ -10,7 +10,7 @@ checked for a missing value, and if a value is missing the default is added. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/endpoints.py b/eve/endpoints.py index 580671f0b..905a9ccb9 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -8,7 +8,7 @@ home) invokes the appropriate method handler, returning its response to the client, properly rendered. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from bson import tz_util diff --git a/eve/exceptions.py b/eve/exceptions.py index b5c8c3b78..9369fba56 100644 --- a/eve/exceptions.py +++ b/eve/exceptions.py @@ -6,7 +6,7 @@ This module implements Eve custom exceptions. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 4f28033ff..d9544d515 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -6,7 +6,7 @@ This module implements the central WSGI application object as a Flask subclass. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import os diff --git a/eve/io/__init__.py b/eve/io/__init__.py index 04930df52..e0f4753d7 100644 --- a/eve/io/__init__.py +++ b/eve/io/__init__.py @@ -6,7 +6,7 @@ This package implements the data layers supported by Eve. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/base.py b/eve/io/base.py index b620af62f..12ed484e4 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -6,7 +6,7 @@ Standard interface implemented by Eve data layers. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import datetime diff --git a/eve/io/media.py b/eve/io/media.py index 135db9d0d..b28eaf536 100644 --- a/eve/io/media.py +++ b/eve/io/media.py @@ -6,7 +6,7 @@ Media storage for Eve-powered APIs. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/__init__.py b/eve/io/mongo/__init__.py index 5e22ddc11..e5d2c6d0e 100644 --- a/eve/io/mongo/__init__.py +++ b/eve/io/mongo/__init__.py @@ -6,7 +6,7 @@ This package implements the MongoDB data layer. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index 53bb1fa16..665025abb 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -6,7 +6,7 @@ Geospatial functions and classes for mongo IO layer - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index f4bf61208..26f538013 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -4,7 +4,7 @@ GridFS media storage for Eve-powered APIs. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from bson import ObjectId diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 05480fcd9..fd21689be 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -6,7 +6,7 @@ The actual implementation of the MongoDB data layer. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import itertools diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index 7e357a583..090767d00 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -7,7 +7,7 @@ This module implements a Python-to-Mongo syntax parser. Allows the MongoDB data-layer to seamlessy respond to a Python-like query. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index a51f756e4..7c595d4fe 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -8,7 +8,7 @@ objects incoming via POST/PATCH requests conform to the API domain. An extension of Cerberus Validator. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import copy diff --git a/eve/methods/__init__.py b/eve/methods/__init__.py index ad7e3d935..9cf0cd478 100644 --- a/eve/methods/__init__.py +++ b/eve/methods/__init__.py @@ -6,7 +6,7 @@ This package implements the HTTP methods supported by Eve. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/common.py b/eve/methods/common.py index fd087e937..85c6b8da7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -6,7 +6,7 @@ Utility functions for API methods implementations. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import base64 diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 1341d021c..a65284c04 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -6,7 +6,7 @@ This module imlements the DELETE method. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/get.py b/eve/methods/get.py index 4856fa2ec..0cf4ea839 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -7,7 +7,7 @@ This module implements the API 'GET' methods, supported by both the resources and single item endpoints. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ import math diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 406ca0a8e..d40db2eba 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -6,7 +6,7 @@ This module imlements the PATCH method. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/post.py b/eve/methods/post.py index 5846866fe..7050d78fa 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -7,7 +7,7 @@ This module imlements the POST method, supported by the resources endopints. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/methods/put.py b/eve/methods/put.py index ef0347eaa..b3e3e246e 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -6,7 +6,7 @@ This module imlements the PUT method. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ from datetime import datetime diff --git a/eve/render.py b/eve/render.py index 53948261e..d9b417762 100644 --- a/eve/render.py +++ b/eve/render.py @@ -6,7 +6,7 @@ Implements proper, automated rendering for Eve responses. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/utils.py b/eve/utils.py index 51efd5919..ced087dc5 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -6,7 +6,7 @@ Utility functions and classes. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ diff --git a/eve/validation.py b/eve/validation.py index 85a9b8f59..88720bb96 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -8,7 +8,7 @@ datalayer-agnostic. Specialized Validator classes are implemented in the datalayer submodules. - :copyright: (c) 2016 by Nicola Iarocci. + :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ From 73998ad36231d755a5656a40cc64a3de957cdcf7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jan 2017 17:46:02 +0100 Subject: [PATCH 099/821] Flask 0.12 support. Closes #963. --- CHANGES | 2 +- eve/tests/logging.py | 1 + requirements.txt | 6 +++--- setup.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 96b983933..c1d809561 100644 --- a/CHANGES +++ b/CHANGES @@ -142,7 +142,7 @@ Version 0.7 - Docs: clarify that Redis is an optional dependency (Mateusz Łoskot). - Update license to 2017. Closes #955. -- Update: Flask 0.11.1 is now supported. Closes #945 and #904. +- Update: Flask 0.12. Closes #945, #904 and #963. Stable ------ diff --git a/eve/tests/logging.py b/eve/tests/logging.py index 3a8d15505..9d86bdd86 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -10,6 +10,7 @@ class TestUtils(TestBase): @log_capture() def test_logging_info(self, l): + self.app.logger.propagate = True self.app.logger.info('test info') l.check( ('eve', 'INFO', 'test info') diff --git a/requirements.txt b/requirements.txt index 503e3a34a..2426fd7fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ Cerberus==0.9.2 Events==0.2.1 Flask-PyMongo==0.4.1 -Flask==0.11.1 +Flask==0.12 itsdangerous==0.24 -Jinja2==2.8 +Jinja2==2.9.4 MarkupSafe==0.23 pymongo==3.2.1 simplejson==3.8.2 -Werkzeug==0.11.11 +Werkzeug==0.11.15 diff --git a/setup.py b/setup.py index 47874279a..ae0bff5c6 100755 --- a/setup.py +++ b/setup.py @@ -9,11 +9,11 @@ 'cerberus>=0.9.2,<0.10', 'events>=0.2.1,<0.3', 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<0.11.11', + 'werkzeug>=0.9.4,<0.11.15', 'markupsafe>=0.23,<1.0', 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', - 'flask>=0.10.1,<=0.11.1', + 'flask>=0.10.1,<=0.12', 'pymongo>=3.2', 'flask-pymongo>=0.4', ] From 8a090c6fd54e80e24363366140cceeb26b655332 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jan 2017 17:57:45 +0100 Subject: [PATCH 100/821] PyMongo 3.4.0 support. Closes #964. --- CHANGES | 1 + requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index c1d809561..560062dbd 100644 --- a/CHANGES +++ b/CHANGES @@ -143,6 +143,7 @@ Version 0.7 - Update license to 2017. Closes #955. - Update: Flask 0.12. Closes #945, #904 and #963. +- Update: PyMongo 3.4 is now required. Closes #964. Stable ------ diff --git a/requirements.txt b/requirements.txt index 2426fd7fd..8d49a7fd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ Flask==0.12 itsdangerous==0.24 Jinja2==2.9.4 MarkupSafe==0.23 -pymongo==3.2.1 +pymongo==3.4.0 simplejson==3.8.2 Werkzeug==0.11.15 diff --git a/setup.py b/setup.py index ae0bff5c6..fe7afd0ac 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', 'flask>=0.10.1,<=0.12', - 'pymongo>=3.2', + 'pymongo>=3.4', 'flask-pymongo>=0.4', ] From 594279c31cf93411a74c9f328cea3187512ff639 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jan 2017 18:02:38 +0100 Subject: [PATCH 101/821] Fix typo in test_create_indexes(). Closes #960. --- CHANGES | 2 ++ eve/tests/config.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 560062dbd..fa6e2fd0b 100644 --- a/CHANGES +++ b/CHANGES @@ -104,6 +104,8 @@ Version 0.7 - Change: ETag response header now conforms to RFC 7232/2.3 and is surrounded by double quotes. Closes #794. +- Fix: ``test_create_indexes()`` typo. Closes 960. + - Fix: fix crash when attempting to modify a document ``_id`` on MongoDB 3.4 (Giorgos Margaritis) diff --git a/eve/tests/config.py b/eve/tests/config.py index d5dcd4bae..c125584b8 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -487,7 +487,7 @@ def test_create_indexes(self): 'mongo_indexes': { 'name': [('name', 1)], 'composed': [('name', 1), ('other_field', 1)], - 'arguments': ([('lat_long', "2d")], {"sparce": True}) + 'arguments': ([('lat_long', "2d")], {"sparse": True}) } } self.app.register_resource('mongodb_features', settings) From d56560613c6959f2f4a20fa17fa88c1b8f1a134d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jan 2017 18:04:07 +0100 Subject: [PATCH 102/821] Add Python 3.6 to README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 82e3df92b..78ed840fd 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ RESTful Web Services. Eve is powered by Flask, Redis, Cerberus, Events and offers support for both MongoDB and SQL backends. -The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5 and PyPy. +The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and PyPy. Eve is Simple ------------- From baca7ae9db3237390fa10805527032c47ea565c0 Mon Sep 17 00:00:00 2001 From: Mario Kralj Date: Sat, 14 Jan 2017 11:05:02 +0000 Subject: [PATCH 103/821] better locating of settings.py Addresses #137. Addresses #209. Addresses #713. Addresses #918. Closes #820. --- .gitignore | 5 +++++ AUTHORS | 1 + docs/config.rst | 30 +++++++++++++++++++++--------- eve/flaskapp.py | 34 ++++++++++++++++++++++++---------- eve/tests/config.py | 7 +++++++ eve/tests/test_settings_env.py | 7 +++++++ 6 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 eve/tests/test_settings_env.py diff --git a/.gitignore b/.gitignore index 9f55cefe9..8754ce46b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ Include Lib Scripts +#pyenv +.python-version + #OSX .Python .DS_Store @@ -60,3 +63,5 @@ _build # PyCharm .idea + +.cache diff --git a/AUTHORS b/AUTHORS index 5ade60f0a..0e53f7ccb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -88,6 +88,7 @@ Patches and Contributions - Marc Abramowitz - Marcus Cobden - Marica Odagaki +- Mario Kralj - Massimo Scamarcia - Mateusz Łoskot - Matt Creenan diff --git a/docs/config.rst b/docs/config.rst index 94e367dad..d4921f618 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -3,13 +3,22 @@ Configuration ============= Generally Eve configuration is best done with configuration files. The -configuration files themselves are actual Python files. +configuration files themselves are actual Python files. However, Eve will +give precedence to dictionary-based settings first, then it will try to +locate a file passed in :envvar:`EVE_SETTINGS` environmental variable (if +set) and finally it will try to locate `settings.py` or a file with filename +passed to `settings` flag in constructor. -Configuration with Files +Configuration With Files ------------------------ -On startup, Eve will look for a `settings.py` file in the application folder. -You can choose an alternative filename/path. Just pass it as an argument when -you instantiate the application. +On startup, if `settings` flag is omitted in constructor, Eve will try to locate +file named `settings.py`, first in the application folder and then in one of the +application's subfolders. You can choose an alternative filename/path, just pass +it as an argument when you instantiate the application. If the file path is +relative, Eve will try to locate it recursively in one of the folders in your +`sys.path`, therefore you have to be sure that your application root is appended +to it. This is useful, for example, in testing environments, when settings file +is not necessarily located in the root of your application. .. code-block:: python @@ -18,12 +27,17 @@ you instantiate the application. app = Eve(settings='my_settings.py') app.run() -Configuration with a Dictionary +Configuration With a Dictionary ------------------------------- -Alternatively, you can choose to provide a settings dictionary: +Alternatively, you can choose to provide a settings dictionary. Unlike +configuring Eve with the settings file, dictionary-based approach will only +update Eve's default settings with your own values, rather than overwriting +all the settings. .. code-block:: python + from eve import Eve + my_settings = { 'MONGO_HOST': 'localhost', 'MONGO_PORT': 27017, @@ -31,8 +45,6 @@ Alternatively, you can choose to provide a settings dictionary: 'DOMAIN': {'contacts': {}} } - from eve import Eve - app = Eve(settings=my_settings) app.run() diff --git a/eve/flaskapp.py b/eve/flaskapp.py index d9544d515..a69cf1bd1 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -9,6 +9,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import fnmatch import os import sys @@ -227,21 +228,34 @@ def load_config(self): if os.path.isabs(self.settings): pyfile = self.settings else: - abspath = os.path.abspath(os.path.dirname(sys.argv[0])) - pyfile = os.path.join(abspath, self.settings) + def find_settings_file(file_name): + # check if we can locate the file from sys.argv[0] + abspath = os.path.abspath(os.path.dirname(sys.argv[0])) + settings_file = os.path.join(abspath, file_name) + if os.path.isfile(settings_file): + return settings_file + else: + # try to find settings.py in one of the + # paths in sys.path + for p in sys.path: + for root, dirs, files in os.walk(p): + for f in fnmatch.filter(files, file_name): + if os.path.isfile(os.path.join(root, f)): + return os.path.join(root, file_name) + + # try to load file from environment variable or settings.py + pyfile = find_settings_file( + os.environ.get('EVE_SETTINGS') or self.settings + ) + + if not pyfile: + raise IOError('Could not load settings.') + try: self.config.from_pyfile(pyfile) - except IOError: - # assume envvar is going to be used exclusively - pass except: raise - # overwrite settings with custom environment variable - envvar = 'EVE_SETTINGS' - if os.environ.get(envvar): - self.config.from_envvar(envvar) - # flask-pymongo compatibility self.config['MONGO_CONNECT'] = self.config['MONGO_OPTIONS'].get( 'connect', True diff --git a/eve/tests/config.py b/eve/tests/config.py index c125584b8..6e2566557 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -95,6 +95,13 @@ def test_settings_as_dict(self): # did not reset other defaults self.assertEqual(self.app.config['MONGO_WRITE_CONCERN'], {'w': 1}) + def test_existing_env_config(self): + env = os.environ + os.environ = {'EVE_SETTINGS': 'test_settings_env.py'} + self.app = Eve() + self.assertTrue('env_domain' in self.app.config['DOMAIN']) + os.environ = env + def test_unexisting_env_config(self): env = os.environ try: diff --git a/eve/tests/test_settings_env.py b/eve/tests/test_settings_env.py new file mode 100644 index 000000000..16554e543 --- /dev/null +++ b/eve/tests/test_settings_env.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +# this is just a helper file which we are going +# to try to load with environmental variable in +# test_existing_env_config() test case + +DOMAIN = {'env_domain': {}} From 347074525c3db4e5fc6a6eec5d639272238ef918 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 17 Jan 2017 14:36:08 +0100 Subject: [PATCH 104/821] Changelog for #962 --- CHANGES | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES b/CHANGES index fa6e2fd0b..64bc61d44 100644 --- a/CHANGES +++ b/CHANGES @@ -104,6 +104,17 @@ Version 0.7 - Change: ETag response header now conforms to RFC 7232/2.3 and is surrounded by double quotes. Closes #794. +- Fix: Better locating of ``settings.py``. On startup, if settings flag is + omitted in constructor, Eve will try to locate file named settings.py, first + in the application folder and then in one of the application's subfolders. + You can choose an alternative filename/path, just pass it as an argument when + you instantiate the application. If the file path is relative, Eve will try + to locate it recursively in one of the folders in your sys.path, therefore + you have to be sure that your application root is appended to it. This is + useful, for example, in testing environments, when settings file is not + necessarily located in the root of your application. Closes #820 (Mario + Kralj). + - Fix: ``test_create_indexes()`` typo. Closes 960. - Fix: fix crash when attempting to modify a document ``_id`` on MongoDB 3.4 From 92fe88b1d0579d5abf78d5ca4b484b00955b5588 Mon Sep 17 00:00:00 2001 From: Kris Lambrechts Date: Fri, 20 Jan 2017 12:26:58 +0100 Subject: [PATCH 105/821] Fix to push auth_field onto versioned documents --- eve/versioning.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/eve/versioning.py b/eve/versioning.py index a78e2749f..1a8727de4 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -118,6 +118,13 @@ def insert_versioning_documents(resource, documents): if not isinstance(documents, list): documents = [documents] + # if 'user-restricted resource access' is enabled and there's + # an Auth request active, inject the username into the document + auth = resource_def['authentication'] + auth_field = resource_def['auth_field'] + if auth and auth_field: + request_auth_value = auth.get_request_auth_value() + # build vesioning documents version = app.config['VERSION'] versioned_documents = [] @@ -134,6 +141,10 @@ def insert_versioning_documents(resource, documents): ver_doc[versioned_id_field(resource_def)] = document[_id] ver_doc[version] = document[version] + # push auth_field + if request_auth_value: + ver_doc[auth_field] = request_auth_value + # add document to the stack versioned_documents.append(ver_doc) From 764ffff0065fbb027cd7ac26698a76bbddcf3da2 Mon Sep 17 00:00:00 2001 From: Kris Lambrechts Date: Fri, 20 Jan 2017 15:06:44 +0100 Subject: [PATCH 106/821] Consider cases without authorization --- eve/versioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eve/versioning.py b/eve/versioning.py index 1a8727de4..014c8c7db 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -120,6 +120,7 @@ def insert_versioning_documents(resource, documents): # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document + request_auth_value = None auth = resource_def['authentication'] auth_field = resource_def['auth_field'] if auth and auth_field: From 9d60a9e65def7810dacdda483bfd8f0ed3c7f122 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 22 Jan 2017 14:58:42 +0100 Subject: [PATCH 107/821] Changelog for #970 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 64bc61d44..03fc7efda 100644 --- a/CHANGES +++ b/CHANGES @@ -115,6 +115,9 @@ Version 0.7 necessarily located in the root of your application. Closes #820 (Mario Kralj). +- Fix: Versioning does not work with User Restricted Resource Access. Closes + #967 (Kris Lambrechts) + - Fix: ``test_create_indexes()`` typo. Closes 960. - Fix: fix crash when attempting to modify a document ``_id`` on MongoDB 3.4 From 6c7f001928062258a33dd2d4552d14e093f8c1ea Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Mon, 30 Jan 2017 23:20:18 +0100 Subject: [PATCH 108/821] Implement tests for CORS regex settings A test was added to address the issue that regexes are not fully matched and the server may let origins that contain extra characters through. Add some tests that introduce a new setting X_DOMAINS_RE which defines a list of regexes to match for when deciding which origins are allowed for CORS. X_DOMAINS should not contain regexes anymore and will only match if the urls in the list are fully equivalent to the origin. Implement a test to check that invalid regexes are ignored --- eve/tests/renders.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index ed67cd871..641be8ede 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -187,6 +187,41 @@ def test_CORS(self): self.assertFalse('http://wwwxgithub.com' in r.headers['Access-Control-Allow-Origin']) + # test that X_DOMAINS does not match if the origin contains extra characters (#974) + r = self.test_client.get('/', headers=[('Origin', 'http://1of2.com:8000')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + + def test_CORS_regex(self): + # test if X_DOMAINS_RE is set with a list of regexes, + # origins are matched against this list (#974) + self.app.config['X_DOMAINS_RE'] = ['^http://sub-\d{3}\.domain\.com$'] + + r = self.test_client.get('/', headers=[('Origin', 'http://sub-123.domain.com')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], + 'http://sub-123.domain.com') + + # test that similar domains are not allowed + r = self.test_client.get('/', headers=[('Origin', 'http://sub-1234.domain.com')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + + r = self.test_client.get('/', headers=[('Origin', 'http://sub-123.domain.com:8000')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + + r = self.test_client.get('/', headers=[('Origin', 'http://sub-123xdomain.com')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + + # test that invalid regexes are ignored, especially '*' + self.app.config['X_DOMAINS_RE'] = ['*'] + r = self.test_client.get('/', headers=[('Origin', 'http://www.example.com')]) + self.assert200(r.status_code) + self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + + def test_CORS_MAX_AGE(self): self.app.config['X_DOMAINS'] = '*' r = self.test_client.get('/', headers=[('Origin', From 2a3d73d45e1eafae27fe3159c6ae31265b620354 Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Mon, 30 Jan 2017 23:46:56 +0100 Subject: [PATCH 109/821] Updating docs for X_DOMAINS_RE --- docs/config.rst | 12 +++++++++--- docs/features.rst | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index d4921f618..6da3575d7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -280,9 +280,15 @@ uppercase. domains are allowed to perform CORS requests. Allowed values are: ``None``, a list of domains, or ``'*'`` for - a wide-open API. Regexes are allowed, which - is useful for websites with dynamic ranges - of subdomains. Defaults to ``None``. + a wide-open API. Defaults to ``None``. + +``X_DOMAINS_RE`` The same setting as ``X_DOMAINS``, but a list + of regexes is allowed. This is useful for + websites with dynamic ranges of + subdomains. Make sure to properly anchor and + escape the regexes. Invalid + regexes (such as ``'*'``) are ignored. + Defaults to ``None``. ``X_HEADERS`` CORS (Cross-Origin Resource Sharing) support. Allows API maintainers to specify which diff --git a/docs/features.rst b/docs/features.rst index 779df3d36..31b50d677 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -789,8 +789,9 @@ Eve-powered APIs can be accessed by the JavaScript contained in web pages. Disabled by default, CORS_ allows web pages to work with REST APIs, something that is usually restricted by most browsers 'same domain' security policy. The ``X_DOMAINS`` setting allows to specify which domains are allowed to perform -CORS requests. Regexes are also allowed, which is useful for websites with -dynamic ranges of subdomains. +CORS requests. A list of regular expressions may be defined in ``X_DOMAINS_RE``, which is useful for websites with dynamic ranges of subdomains. Make sure to +anchor and escape the regexes properly, for example +``X_DOMAINS_RE = ['^http://sub-\d{3}\.example\.com$']``. JSONP Support ------------- From 9ccd85816b1d0f12f8ceb72e8d28cc1a4ae5c03b Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Tue, 31 Jan 2017 00:48:11 +0100 Subject: [PATCH 110/821] Implement the X_DOMAINS_RE setting X_DOMAINS_RE contains regexes to allow in CORS. Changed X_DOMAINS back to only allow equivalent strings. Invalid regexes are silently ignored. --- eve/default_settings.py | 2 ++ eve/render.py | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index fd3222eeb..6da59d429 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -15,6 +15,7 @@ 'OPTIMIZE_PAGINATION_FOR_SPEED' added and set to False. 'OPLOG_RETURN_EXTRA_FIELD' added and set to False. 'ENFORCE_IF_MATCH'added and set to True. + 'X_DOMAINS_RE' added and set to None .. versionchanged:: 0.6 'UPSERT_ON_PUT? added and set to True. @@ -138,6 +139,7 @@ CACHE_EXPIRES = 0 ITEM_CACHE_CONTROL = '' X_DOMAINS = None # CORS disabled by default. +X_DOMAINS_RE = None # CORS disabled by default. X_HEADERS = None # CORS disabled by default. X_EXPOSE_HEADERS = None # CORS disabled by default. X_ALLOW_CREDENTIALS = None # CORS disabled by default. diff --git a/eve/render.py b/eve/render.py index d9b417762..635f7ffc9 100644 --- a/eve/render.py +++ b/eve/render.py @@ -109,7 +109,7 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, :param status: response status. .. versionchanged:: 0.7 - Add support for regexes in X_DOMAINS values. Closes #660. + Add support for regexes in X_DOMAINS_RE. Closes #660, #974. ETag value now surrounded by double quotes. Closes #794. .. versionchanged:: 0.6 @@ -187,12 +187,29 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, # CORS origin = request.headers.get('Origin') - if origin and config.X_DOMAINS: - if isinstance(config.X_DOMAINS, str): + if origin and (config.X_DOMAINS or config.X_DOMAINS_RE): + if config.X_DOMAINS is None: + domains = [] + elif isinstance(config.X_DOMAINS, str): domains = [config.X_DOMAINS] else: domains = config.X_DOMAINS + if config.X_DOMAINS_RE is None: + domains_re = [] + elif isinstance(config.X_DOMAINS_RE, str): + domains_re = [config.X_DOMAINS_RE] + else: + domains_re = config.X_DOMAINS_RE + + # precompile regexes and ignore invalids + domains_re_compiled = [] + for domain_re in domains_re: + try: + domains_re_compiled.append(re.compile(domain_re)) + except re.error: + continue + if config.X_HEADERS is None: headers = [] elif isinstance(config.X_HEADERS, str): @@ -216,7 +233,9 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, if '*' in domains: resp.headers.add('Access-Control-Allow-Origin', origin) resp.headers.add('Vary', 'Origin') - elif any(re.match(re.escape(domain), origin) for domain in domains): + elif any(origin == domain for domain in domains): + resp.headers.add('Access-Control-Allow-Origin', origin) + elif any(domain.match(origin) for domain in domains_re_compiled): resp.headers.add('Access-Control-Allow-Origin', origin) else: resp.headers.add('Access-Control-Allow-Origin', '') From d7e6652784eef97d9141c5e8b1583d3c8035ed5e Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Tue, 31 Jan 2017 01:04:55 +0100 Subject: [PATCH 111/821] update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 0e53f7ccb..e0cc6015b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Patches and Contributions - Dougal Matthews - Emmanuel Leblond - Eugene Prikazchikov +- Felix Peppert - Florian Rathgeber - Francisco Corrales Morales - Garrin Kimmell From 90e79bd0feb5a35a749f256ea442368f691b17bc Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Tue, 31 Jan 2017 01:07:13 +0100 Subject: [PATCH 112/821] Update CHANGES --- CHANGES | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 03fc7efda..78201f260 100644 --- a/CHANGES +++ b/CHANGES @@ -47,8 +47,8 @@ Version 0.7 - New: ``MONGO_OPTIONS`` allows MongoDB arguments to be passed to the MongoClient object. Defaults to ``{}`` (Massimo Scamarcia). -- New: Regexes are allowed when setting ``X_DOMAINS`` values. This allows CORS - to support websites with dynamic ranges of subdomains. Closes #660. +- New: Regexes are allowed by setting ``X_DOMAINS_RE`` values. This allows CORS + to support websites with dynamic ranges of subdomains. Closes #660 and #974. - New: If ``ENFORCE_IF_MATCH`` option is active, then all requests are expected to include the ``If-Match`` or they will be rejected (same as old behavior). From ebb24be50c6cbcfcbb816627b02754404be4c119 Mon Sep 17 00:00:00 2001 From: Felix Peppert Date: Tue, 31 Jan 2017 01:34:24 +0100 Subject: [PATCH 113/821] fix pep8 errors --- eve/tests/renders.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 641be8ede..fbdd6e776 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -187,8 +187,10 @@ def test_CORS(self): self.assertFalse('http://wwwxgithub.com' in r.headers['Access-Control-Allow-Origin']) - # test that X_DOMAINS does not match if the origin contains extra characters (#974) - r = self.test_client.get('/', headers=[('Origin', 'http://1of2.com:8000')]) + # test that X_DOMAINS does not match + # if the origin contains extra characters (#974) + r = self.test_client.get('/', headers=[('Origin', + 'http://1of2.com:8000')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') @@ -197,31 +199,35 @@ def test_CORS_regex(self): # origins are matched against this list (#974) self.app.config['X_DOMAINS_RE'] = ['^http://sub-\d{3}\.domain\.com$'] - r = self.test_client.get('/', headers=[('Origin', 'http://sub-123.domain.com')]) + r = self.test_client.get('/', headers=[('Origin', + 'http://sub-123.domain.com')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], 'http://sub-123.domain.com') # test that similar domains are not allowed - r = self.test_client.get('/', headers=[('Origin', 'http://sub-1234.domain.com')]) + r = self.test_client.get('/', headers=[('Origin', + 'http://sub-1234.domain.com')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') - r = self.test_client.get('/', headers=[('Origin', 'http://sub-123.domain.com:8000')]) + r = self.test_client.get( + '/', headers=[('Origin', 'http://sub-123.domain.com:8000')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') - r = self.test_client.get('/', headers=[('Origin', 'http://sub-123xdomain.com')]) + r = self.test_client.get('/', headers=[('Origin', + 'http://sub-123xdomain.com')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') # test that invalid regexes are ignored, especially '*' self.app.config['X_DOMAINS_RE'] = ['*'] - r = self.test_client.get('/', headers=[('Origin', 'http://www.example.com')]) + r = self.test_client.get('/', headers=[('Origin', + 'http://www.example.com')]) self.assert200(r.status_code) self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') - def test_CORS_MAX_AGE(self): self.app.config['X_DOMAINS'] = '*' r = self.test_client.get('/', headers=[('Origin', From b6da79e6a65e6dad040fa87c5e7f7eabf4da6427 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Feb 2017 15:31:58 +0100 Subject: [PATCH 114/821] Add Python 3.6 to list of Trove classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index fe7afd0ac..48e58bd97 100755 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], ) From 47bb7d11ec50fae706b4b655e4c5e0e74c850000 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Feb 2017 15:32:24 +0100 Subject: [PATCH 115/821] Bump version to 0.7 --- CHANGES | 9 ++++++--- eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 78201f260..8c1fe3bda 100644 --- a/CHANGES +++ b/CHANGES @@ -6,8 +6,14 @@ Here you can see the full list of changes between each Eve release. In Development -------------- +Stable +------ + Version 0.7 ~~~~~~~~~~~ + +Released on 6 February, 2017 + - New: Add Python 3.6 as a supported interpreter. - New: ``OPTIMIZE_PAGINATION_FOR_SPEED``. Set this to ``True`` to improve @@ -161,9 +167,6 @@ Version 0.7 - Update: Flask 0.12. Closes #945, #904 and #963. - Update: PyMongo 3.4 is now required. Closes #964. -Stable ------- - Version 0.6.4 ~~~~~~~~~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index 5790633ea..25acf9bb9 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.dev0' +__version__ = '0.7' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 48e58bd97..be33f3458 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.dev0', + version='0.7', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From f075180f2b78c5804838022d3f2405ca731535c5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Feb 2017 17:13:43 +0100 Subject: [PATCH 116/821] Bump verstion to 0.7.1-dev --- CHANGES | 8 ++++---- eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index d14039c9d..93e8fb27a 100644 --- a/CHANGES +++ b/CHANGES @@ -3,13 +3,13 @@ Changelog Here you can see the full list of changes between each Eve release. -Stable ------- +Development +----------- -Version 0.6.3 +Version 0.7.1 ~~~~~~~~~~~~~ -- Fix: Since 0.6.2, static projections are not honoured. Closes #837. +- *hic sunt dracones* Stable ------ diff --git a/eve/__init__.py b/eve/__init__.py index 25acf9bb9..c1495b396 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7' +__version__ = '0.7.1-dev' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index be33f3458..5d1f56dfd 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7', + version='0.7.1-dev', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From c34e2313e067b4d4de367384962bd2deab56cb3e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Feb 2017 17:29:41 +0100 Subject: [PATCH 117/821] drop 'develop' branch. 'master' is now the default. --- .travis.yml | 1 - CHANGES | 3 +++ CONTRIBUTING.rst | 5 ++--- docs/license.rst | 2 +- docs/snippets/index.rst | 6 +++--- docs/testing.rst | 14 +++++++------- docs/validation.rst | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index f276c95af..e6b091ee7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,4 +31,3 @@ before_script: branches: only: - master - - develop diff --git a/CHANGES b/CHANGES index 93e8fb27a..5fb729e90 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,9 @@ Version 0.7.1 - *hic sunt dracones* +- ``develop`` branch has been dropped. ``master`` is now the default project + branch. + Stable ------ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ebf68d349..c4e0b6835 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -16,8 +16,7 @@ Making Changes -------------- * Fork_ the repository on GitHub. * Create a topic branch from where you want to base your work. -* This is usually the ``develop`` branch. -* Please avoid working directly on the ``develop`` branch. +* This is usually the ``master`` branch. * Make commits of logical units (if needed rebase your feature branch before submitting it). * Check for unnecessary whitespace with ``git diff --check`` before committing. @@ -63,7 +62,7 @@ case, other than GitHub help_ pages, you might want to check this excellent `Effective Guide to Pull Requests`_ .. _`the repository`: http://github.com/nicolaiarocci/eve -.. _AUTHORS: https://github.com/nicolaiarocci/eve/blob/develop/AUTHORS +.. _AUTHORS: https://github.com/nicolaiarocci/eve/blob/master/AUTHORS .. _`open issues`: https://github.com/nicolaiarocci/eve/issues .. _`new issue`: https://github.com/nicolaiarocci/eve/issues/new .. _GitHub: https://github.com/ diff --git a/docs/license.rst b/docs/license.rst index b25084ac6..78be6c338 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -15,4 +15,4 @@ Artwork License Eve artwork 2013 by Roberto Pasini "Kalamun" released under the `Creative Commons BY-SA`_ license. -.. _`Creative Commons BY-SA`: https://github.com/nicolaiarocci/eve/blob/develop/artwork/LICENSE +.. _`Creative Commons BY-SA`: https://github.com/nicolaiarocci/eve/blob/master/artwork/LICENSE diff --git a/docs/snippets/index.rst b/docs/snippets/index.rst index 222f2d161..a85edd06c 100644 --- a/docs/snippets/index.rst +++ b/docs/snippets/index.rst @@ -26,7 +26,7 @@ source_), and then submit a `pull request`_. template -.. _template: https://raw.githubusercontent.com/nicolaiarocci/eve/develop/docs/snippets/template.rst +.. _template: https://raw.githubusercontent.com/nicolaiarocci/eve/master/docs/snippets/template.rst .. _`pull request`: https://github.com/nicolaiarocci/eve/pulls -.. _`snippets folder`: https://github.com/nicolaiarocci/eve/tree/develop/docs/snippets -.. _source: https://raw.githubusercontent.com/nicolaiarocci/eve/develop/docs/snippets/index.rst +.. _`snippets folder`: https://github.com/nicolaiarocci/eve/tree/master/docs/snippets +.. _source: https://raw.githubusercontent.com/nicolaiarocci/eve/master/docs/snippets/index.rst diff --git a/docs/testing.rst b/docs/testing.rst index f4dcee7c8..b249b1206 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -135,18 +135,18 @@ yourself with Continuous Integration ---------------------- -Each time code is pushed to either the ``develop`` or the ``master`` branch -the whole test-suite is executed on Travis-CI. This is also the case for -pull-requests. When a pull request is submitted and the CI run fails two things -happen: a 'the build is broken' email is sent to the submitter; the request is -rejected. The contributor can then fix the code, add one or more commits as -needed, and push again. +Each time code is pushed to the ``master`` branch the whole test-suite is +executed on Travis-CI. This is also the case for pull-requests. When a pull +request is submitted and the CI run fails two things happen: a 'the build is +broken' email is sent to the submitter; the request is rejected. The +contributor can then fix the code, add one or more commits as needed, and push +again. The CI will also run flake8 so make sure that your code complies to PEP8 before submitting a pull request, or be prepared to be mail-spammed by CI. Please note that in practice you're only supposed to submit pull requests -against the ``develop`` branch, see :ref:`contributing`. +against the ``master`` branch, see :ref:`contributing`. Building documentation ---------------------- diff --git a/docs/validation.rst b/docs/validation.rst index 46d615630..0a55b88c6 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -198,6 +198,6 @@ There are two ways to deal with non-conforming schemas: to disable schema validation for a given endpoint. .. _Cerberus: http://python-cerberus.org -.. _`source code`: https://github.com/nicolaiarocci/eve/blob/develop/eve/io/mongo/validation.py +.. _`source code`: https://github.com/nicolaiarocci/eve/blob/master/eve/io/mongo/validation.py .. _`function-based validation`: http://docs.python-cerberus.org/en/latest/customize.html#function-validator .. _`type coercion`: http://docs.python-cerberus.org/en/latest/usage.html#type-coercion From 27001bd201ba086927e9ae72c90d62f3631d4bb2 Mon Sep 17 00:00:00 2001 From: Dominik Kellner Date: Fri, 10 Feb 2017 23:59:11 +0700 Subject: [PATCH 118/821] Fix typo and deadlink --- docs/_templates/sidebarintro.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index f9e2dfb3a..2a673e687 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -27,9 +27,9 @@

Useful Links

  • Eve @ GitHub
  • Eve @ Stack Overflow
  • Eve @ Google Groups
  • -
  • Eve @IRC +
  • Eve @ IRC
  • Eve @ PyPI
  • -
  • Eve @ Nicola Iarocci
  • +
  • Eve @ Nicola Iarocci
  • Issue Tracker
  • You are looking at the documentation of the development version.

    From 1b85ac4f01b9c819788b1a802edd6e7ce8da1d25 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 Feb 2017 08:05:39 +0100 Subject: [PATCH 119/821] Changelog for #985 --- CHANGES | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 5fb729e90..6b8d07d64 100644 --- a/CHANGES +++ b/CHANGES @@ -9,8 +9,7 @@ Development Version 0.7.1 ~~~~~~~~~~~~~ -- *hic sunt dracones* - +- Docs: fix typo and dead link to Nicola's website (Dominik Kellner). - ``develop`` branch has been dropped. ``master`` is now the default project branch. From d4d78800ca46fc17b52ec835ce1a019b092aef15 Mon Sep 17 00:00:00 2001 From: Sobolev Nikita Date: Sat, 11 Feb 2017 13:50:12 +0300 Subject: [PATCH 120/821] Updates README.rst with svg badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 78ed840fd..450b90e40 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ Eve ==== -.. image:: https://secure.travis-ci.org/nicolaiarocci/eve.png?branch=master +.. image:: https://secure.travis-ci.org/nicolaiarocci/eve.svg?branch=master :target: https://secure.travis-ci.org/nicolaiarocci/eve Eve is an open source Python REST API framework designed for human beings. It From d86c4e5a42bd8121d0023670e2fda894476fbc42 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 13 Feb 2017 17:41:12 +0100 Subject: [PATCH 121/821] Sobolev Nikita --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e0cc6015b..73e545635 100644 --- a/AUTHORS +++ b/AUTHORS @@ -130,6 +130,7 @@ Patches and Contributions - Sebastien Estienne - Sebastián Magrí - Simon Schönfeld +- Sobolev Nikita - Stanislav Filin - Stanislav Heller - Stratos Gerakakis From bb4d0e9e04620e148b01aec2f5b04bb3fef5f6eb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 13 Feb 2017 17:42:01 +0100 Subject: [PATCH 122/821] Changelog for #986 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 6b8d07d64..25dae9385 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,9 @@ Development Version 0.7.1 ~~~~~~~~~~~~~ +- Docs: update README with svg bade (Sobolev Nikita). - Docs: fix typo and dead link to Nicola's website (Dominik Kellner). + - ``develop`` branch has been dropped. ``master`` is now the default project branch. From 27adc084bc1fcc9447bfdb76b85d3c092cc2d118 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Feb 2017 07:44:07 +0100 Subject: [PATCH 123/821] Fix: cannot create consistent MRO. Closes #984. --- CHANGES | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 25dae9385..bc20e353c 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,9 @@ Development Version 0.7.1 ~~~~~~~~~~~~~ +- Fix: "Cannot create a consistent method resolution order" on Python 3.5.2 and + 3.6 since Eve 0.7. Closes #984. + - Docs: update README with svg bade (Sobolev Nikita). - Docs: fix typo and dead link to Nicola's website (Dominik Kellner). diff --git a/setup.py b/setup.py index 5d1f56dfd..3ffe11f84 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'cerberus>=0.9.2,<0.10', 'events>=0.2.1,<0.3', 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<0.11.15', + 'werkzeug>=0.9.4,<=0.11.15', 'markupsafe>=0.23,<1.0', 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', From d3ebbe7f357aa012f1f604adea787f4b43523432 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Feb 2017 08:14:46 +0100 Subject: [PATCH 124/821] v0.7.1 release date --- CHANGES | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index bc20e353c..5cc0abc76 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,16 @@ Development Version 0.7.1 ~~~~~~~~~~~~~ +- *hic sunt dracones* + +Stable +------ + +Version 0.7.1 +~~~~~~~~~~~~~ + +Released on 14 February, 2017 + - Fix: "Cannot create a consistent method resolution order" on Python 3.5.2 and 3.6 since Eve 0.7. Closes #984. @@ -18,9 +28,6 @@ Version 0.7.1 - ``develop`` branch has been dropped. ``master`` is now the default project branch. -Stable ------- - Version 0.7 ~~~~~~~~~~~ From 4e69c01cc72a69e311fb01ae7945a108a7af8a4c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Feb 2017 08:16:21 +0100 Subject: [PATCH 125/821] Bump verstion to 0.7.1 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index c1495b396..7bb3713d8 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.1-dev' +__version__ = '0.7.1' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 3ffe11f84..506e70d2b 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.1-dev', + version='0.7.1', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 4b74531e501635a93386822cf7cb7635d4b08315 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Feb 2017 09:29:52 +0100 Subject: [PATCH 126/821] Bump version to 0.7.2-dev --- CHANGES | 2 +- eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 5cc0abc76..1c7e87c00 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. Development ----------- -Version 0.7.1 +Version 0.7.2 ~~~~~~~~~~~~~ - *hic sunt dracones* diff --git a/eve/__init__.py b/eve/__init__.py index 7bb3713d8..af797d17c 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.1' +__version__ = '0.7.2-dev' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 506e70d2b..e2c1f0529 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.1', + version='0.7.2-dev', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 2a5e6cdf7ed269c2ab92cbdc532de2cfc0ec9ef5 Mon Sep 17 00:00:00 2001 From: John Chang Date: Sat, 4 Feb 2017 18:58:50 +0100 Subject: [PATCH 127/821] Add snippet that implements a list of embedded items --- docs/snippets/index.rst | 5 +- docs/snippets/list_of_items.rst | 158 ++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 docs/snippets/list_of_items.rst diff --git a/docs/snippets/index.rst b/docs/snippets/index.rst index a85edd06c..aaa62326b 100644 --- a/docs/snippets/index.rst +++ b/docs/snippets/index.rst @@ -4,7 +4,7 @@ Snippets ======== Welcome to the Eve snippet archive. This is the place where anyone can drop -helpful pieces of code for others to use. +helpful pieces of code for others to use. Available Snippets ------------------ @@ -13,7 +13,8 @@ Available Snippets :maxdepth: 2 hooks_blueprints - + list_of_items + Add your snippet ---------------- diff --git a/docs/snippets/list_of_items.rst b/docs/snippets/list_of_items.rst new file mode 100644 index 000000000..10fe4dab4 --- /dev/null +++ b/docs/snippets/list_of_items.rst @@ -0,0 +1,158 @@ +List of Items +================ +by John Chang + +This is an example of how to implement a simple list of items that supports both list-level and item-level CRUD operations. + +Specifically, it should be possible to use a single GET to get the entire list (including all items) but also a single POST to append an item (rather than PATCHing the list). + +The solution was to database event hooks to inject the embedded child documents (``items``) into the parent list before it's returned to the client and also delete the child items when the parent list is deleted. This works, although it results in two DB queries. + +main.py +--------------- +.. code-block:: python + + from eve import Eve + from pymongo import MongoClient + from bson.objectid import ObjectId + + app = Eve() + + client = MongoClient('localhost', 27017) + db = client.evedemo + + + def after_fetching_lists(response): + list_id = response['_id'] + response['items'] = list(db.items.find({'list_id': ObjectId(list_id)})) + + + def after_deleting_lists(item): + list_id = item['_id'] + db.items.delete_many({'list_id': ObjectId(list_id)}) + + app.on_fetched_item_lists += after_fetching_lists + app.on_deleted_item_lists += after_deleting_lists + + app.run() + +settings.py +--------------- +.. code-block:: python + + import os + + DEBUG = True + + MONGO_HOST = os.environ.get('MONGO_HOST', 'localhost') + MONGO_PORT = os.environ.get('MONGO_PORT', 27017) + MONGO_USERNAME = os.environ.get('MONGO_USERNAME', 'user') + MONGO_PASSWORD = os.environ.get('MONGO_PASSWORD', 'user') + MONGO_DBNAME = os.environ.get('MONGO_DBNAME', 'listtest') + + RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] + ITEM_METHODS = ['GET', 'PUT', 'PATCH', 'DELETE'] + + lists = { + 'schema': { + 'title': { + 'type': 'string' + } + } + } + + items = { + 'url': 'lists//items', + 'schema': { + 'list_id': { + 'type': 'objectid', + 'required': True, + 'data_relation': { + 'resource': 'lists', + 'field': '_id' + } + }, + 'name': {'type': 'string', + 'required': True + } + } + } + + DOMAIN = { + 'lists': lists, + 'items': items + } + +Usage +--------------- +.. code-block:: bash + + $ curl -i -X POST http://127.0.0.1:5000/lists -d title="My List" + HTTP/1.0 201 CREATED + + { + "_id": "58960f83a663e2e6746dfa6a", + : + } + + $ curl -i -X POST http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items -d 'name=Alice' + HTTP/1.0 201 CREATED + + $ curl -i -X POST http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items -d 'name=Bob' + HTTP/1.0 201 CREATED + + $ curl -i -X GET http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a + HTTP/1.0 200 OK + + { + "_created": "Sat, 04 Feb 2017 17:29:39 GMT", + "_etag": "01799f6be25a044ab95cfeb2dc0f834d11b796d8", + "_id": "58960f83a663e2e6746dfa6a", + "_updated": "Sat, 04 Feb 2017 17:29:39 GMT", + "items": [ + { + "_created": "Sat, 04 Feb 2017 17:30:06 GMT", + "_etag": "72ad9248ad5bf45c7bfe3e03a1b9bc384d94572f", + "_id": "58960f9ea663e2e6746dfa6b", + "_updated": "Sat, 04 Feb 2017 17:30:06 GMT", + "list_id": "58960f83a663e2e6746dfa6a", + "name": "Alice", + "quantity": 1 + }, + { + "_created": "Sat, 04 Feb 2017 17:30:13 GMT", + "_etag": "447f51b057fb5e0a70472e96ff883c64b5e2e308", + "_id": "58960fa5a663e2e6746dfa6c", + "_updated": "Sat, 04 Feb 2017 17:30:13 GMT", + "list_id": "58960f83a663e2e6746dfa6a", + "name": "Bob", + "quantity": 1 + } + ], + "title": "My List" + } + + $ curl -i -X DELETE http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items/58960f9ea663e2e6746dfa6b -H "If-Match: 72ad9248ad5bf45c7bfe3e03a1b9bc384d94572f" + HTTP/1.0 204 NO CONTENT + + $ curl -i -X GET http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a + HTTP/1.0 200 OK + + { + "_created": "Sat, 04 Feb 2017 17:29:39 GMT", + "_etag": "01799f6be25a044ab95cfeb2dc0f834d11b796d8", + "_id": "58960f83a663e2e6746dfa6a", + "_updated": "Sat, 04 Feb 2017 17:29:39 GMT", + "items": [ + { + "_created": "Sat, 04 Feb 2017 17:30:13 GMT", + "_etag": "447f51b057fb5e0a70472e96ff883c64b5e2e308", + "_id": "58960fa5a663e2e6746dfa6c", + "_updated": "Sat, 04 Feb 2017 17:30:13 GMT", + "list_id": "58960f83a663e2e6746dfa6a", + "name": "Bob", + "quantity": 1 + } + ], + "title": "My List" + } From 39802411472e7be31cbb8dd1bb46ca83a97372e3 Mon Sep 17 00:00:00 2001 From: John Chang Date: Mon, 6 Feb 2017 11:19:16 +0100 Subject: [PATCH 128/821] Use app.data.driver instead of MongoClient --- docs/snippets/list_of_items.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/snippets/list_of_items.rst b/docs/snippets/list_of_items.rst index 10fe4dab4..91f8a80db 100644 --- a/docs/snippets/list_of_items.rst +++ b/docs/snippets/list_of_items.rst @@ -13,23 +13,22 @@ main.py .. code-block:: python from eve import Eve - from pymongo import MongoClient from bson.objectid import ObjectId app = Eve() - - client = MongoClient('localhost', 27017) - db = client.evedemo + mongo = app.data.driver def after_fetching_lists(response): list_id = response['_id'] - response['items'] = list(db.items.find({'list_id': ObjectId(list_id)})) + f = {'list_id': ObjectId(list_id)} + response['items'] = list(mongo.db.items.find(f)) def after_deleting_lists(item): list_id = item['_id'] - db.items.delete_many({'list_id': ObjectId(list_id)}) + f = {'list_id': ObjectId(list_id)} + mongo.db.items.delete_many(f) app.on_fetched_item_lists += after_fetching_lists app.on_deleted_item_lists += after_deleting_lists From f16f110f8c12df14fff3602835a225c38deeeede Mon Sep 17 00:00:00 2001 From: John Chang Date: Mon, 6 Feb 2017 14:38:30 +0100 Subject: [PATCH 129/821] Fix "list_id": "required field" --- docs/snippets/list_of_items.rst | 46 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/docs/snippets/list_of_items.rst b/docs/snippets/list_of_items.rst index 91f8a80db..6ba50e395 100644 --- a/docs/snippets/list_of_items.rst +++ b/docs/snippets/list_of_items.rst @@ -52,36 +52,32 @@ settings.py RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] ITEM_METHODS = ['GET', 'PUT', 'PATCH', 'DELETE'] - lists = { - 'schema': { - 'title': { - 'type': 'string' + DOMAIN = { + 'lists': { + 'schema': { + 'title': { + 'type': 'string' + } } - } - } - - items = { - 'url': 'lists//items', - 'schema': { - 'list_id': { - 'type': 'objectid', - 'required': True, - 'data_relation': { - 'resource': 'lists', - 'field': '_id' + }, + 'items': { + 'url': 'lists//items', + 'schema': { + 'list_id': { + 'type': 'objectid', + 'data_relation': { + 'resource': 'lists', + 'field': '_id' + } + }, + 'name': { + 'type': 'string', + 'required': True } - }, - 'name': {'type': 'string', - 'required': True - } + } } } - DOMAIN = { - 'lists': lists, - 'items': items - } - Usage --------------- .. code-block:: bash From 1272dbfe19b0a420f9036365bbd76eb0d62f07c9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 19 Feb 2017 08:50:14 +0100 Subject: [PATCH 130/821] Changelog for #981 --- CHANGES | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 1c7e87c00..fdff2a358 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,9 @@ Development Version 0.7.2 ~~~~~~~~~~~~~ -- *hic sunt dracones* +- Docs: Add code snippet with an example of how to implement a simple list of + items that supports both list-level and item-level CRUD operations (John + Chang). Stable ------ From 47ed7bf84898b42d7d4c9cfeb81ca0541dee9ca6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 19 Feb 2017 08:50:44 +0100 Subject: [PATCH 131/821] John Chang --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 73e545635..8c35e2db9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -63,6 +63,7 @@ Patches and Contributions - Jen Montes - Joakim Uddholm - Johan Bloemberg +- John Chang - John Deng - Jorge Morales - Jorge Puente Sarrín From 4695af7f6eda400e770a235e0375f78f8f15daa9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 19 Feb 2017 08:54:50 +0100 Subject: [PATCH 132/821] Change Snippets page index maxdepth --- docs/snippets/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/snippets/index.rst b/docs/snippets/index.rst index aaa62326b..44c3d77eb 100644 --- a/docs/snippets/index.rst +++ b/docs/snippets/index.rst @@ -10,7 +10,7 @@ Available Snippets ------------------ .. toctree:: - :maxdepth: 2 + :maxdepth: 1 hooks_blueprints list_of_items From f7b25c0044a118e08909b82ba56744852fa3ffac Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 19 Feb 2017 08:55:09 +0100 Subject: [PATCH 133/821] Minor edits for new list-of-items snippets --- docs/snippets/list_of_items.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/snippets/list_of_items.rst b/docs/snippets/list_of_items.rst index 6ba50e395..f77b277c4 100644 --- a/docs/snippets/list_of_items.rst +++ b/docs/snippets/list_of_items.rst @@ -1,5 +1,5 @@ -List of Items -================ +Supporting both list-level and item-level CRUD operations +========================================================= by John Chang This is an example of how to implement a simple list of items that supports both list-level and item-level CRUD operations. @@ -9,7 +9,7 @@ Specifically, it should be possible to use a single GET to get the entire list ( The solution was to database event hooks to inject the embedded child documents (``items``) into the parent list before it's returned to the client and also delete the child items when the parent list is deleted. This works, although it results in two DB queries. main.py ---------------- +------- .. code-block:: python from eve import Eve @@ -36,7 +36,7 @@ main.py app.run() settings.py ---------------- +----------- .. code-block:: python import os @@ -79,7 +79,7 @@ settings.py } Usage ---------------- +----- .. code-block:: bash $ curl -i -X POST http://127.0.0.1:5000/lists -d title="My List" From 281e93efc7156b3bfc68055a6ba60e1e61b09801 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 3 Mar 2017 11:55:49 +0100 Subject: [PATCH 134/821] Add eve leaf to artwork --- artwork/eve_leaf.png | Bin 0 -> 22086 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 artwork/eve_leaf.png diff --git a/artwork/eve_leaf.png b/artwork/eve_leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..cd01dab322cd4aec2e535c4a4ea9792322aec244 GIT binary patch literal 22086 zcmZs?1z4NGvn~!P6fLgBo#IY$cX#(-#a)8CLve>9#Vxo)ad&s8xJxN;)89Gg-usv2 z$&)Yp?at1;yV;$0cD_huMX67SpAn&;pgze+i>pFGLF+(1dhj11CH}mx(~vJ{7gZ@y zsOm|=-;nDuYjtf`Z3TH=GY2~+6QF~sIg_WIBcv7-6u&1g!K*4R`QObUzXZsw zTwNV`nVCI2JeWM#nH-!gnOS*wc$it(nAzAEAvG9XyzE^~JQ?j>DE>>x|Hu(HcQJFe zc67CNuqXW|*TmGp%~gP${9i}^`}N;?+ByF3j_h6j+bW2C%$_EW%&bf-%>RFlT&*qs zk9z-)R{!q&SF`_E^|6Sw% zXv+UjB(IXQwK>Gre-;U{@-zQ`%Kp1PKl49x|1b0Z3)}ycLckS7g!J@310{&~g=s$hXmTAn4l(b5w3obQed7$^hoArz`+40P`&zfa%qRP7s!R}r8=WRCkaBZl5d zO}rG!okiXh;)>TV`nDJmi+kv5i)gXZb&f1e?1Yh=W2$G|B&VLgGyZr}7mv$FhCq?^ zL;!!29ZNw`wjazZ?F1FDj*c3^gSg{5891CGS1Epz@IAc7OUS zV{}?Tlr#^GG;yM2anjgkA~ADnRQ}D4KUxlJxoCu{GJhIVN%f9#2#A9$@o191vtUK} z;F`DgvK(f^N*YI{-~G5Q zN(}Zy;9=yFT`)^ zjAgt^#=pYk+put2Sjo7S)40_91TLJK>y%km5sfwut$zVNC>mVIjo~u3%stJbXbG;pI1(gnb3KSKV61(P?9X-di2KXf8fzX+ zm%@z*XRcB>0%@j-;TpjHWFN180SlRP>n)|O1Q%%+K zjWkVb1KryZ?K=V#iS{cm3UV3$7>R-KC8KxI3QaHdI5g#82x6ZbZb%&tW=B7(;Al)8 z#*=8l8?6S|p&>pF{1M$b_DA2mrsARddsjwa=sCwVnpDsv8X&!>Ibq%b)>zVjg~^)^ z&D8&9Lm)6ZH5kV|n5J39^zD{n7SDqKL~xD!7Q_-C&;Obu!I59w_V)iw>cQ!(P1=84s=cj9)tpes_>O@9jST^%9E#ouEL(6V z%hmL7C%FQ;Y+`31AC`=4=q1sc43r{)p-Eo;L*C#itOVN3jrV+octi6wv8Sx zUrRT0F7TvT(ph1??sGIpS9ZGaQAmI)Oaw56JjMSRocUK4b_uQ>w4A$f7>YF=s1G;Z zQ!65Ne%~kt$V3I^V~Ub~f{3;b7aU(vW|c?Pp`!XEZ?iF{|qhJ@({FE!&yD!^I^kk5v($W_g>&PZcQq zlz;;>ck&e%wBH}nLK?gH_m8PIklQT;j5R;=jrf(1loETV3Z8YiZIuz)(wXVe}{t^&_Gk;Tn z{N2`&!^K^)Eu9s;6+AbZSX@)8qszXKnE73mM=U^^%W`_HWH+`e)Rws7cqUXsuHvwq zP9%dU5=vsn8Cd_>Ono7-9F^tp>n7gsxa8GUqI1FfMvWSxD9=4<%#qR^wEWyNNBEgn zXc%X728(=WP_8l*KBy_HS{HNV1hJ$(l&pb7!LQ8L6p^$Lg9XllCeh8f@UsO*tCMwL zDqz@3WJ1I|0!TEUE}4aa6*Ql;@X^@~vLiFw(90H|#X%|ftaQREERw9vTrC*UWa(GE zR$wkeJ1m2o_f7_VAGpI9Aj|DNa3Np9iwCgJTyemY55=ad>>`d&t|P@!=p~sDE~2Te z29;5{U2kbZt^;b)n{0?fU_BYJ2o!{jG=M(~2ZDoj-+}Bo`Ub_5JI9Xxg2SUXWSBPe zlsn%zu8WE5JD3AUm@mv7H8(%}APNwSlp)#Z@VN!8N+`>;lL`yyAT`hb)tH<5Gd}9V z+?*f90ZcF`gb}CSOv-_$v%`hVXqz9Ym%DIoHUe_}#e^Qu zscTxA!O?Z=Br|-=5R&O1i{;MxXd)|A5vNwT)9n%m!zP|bGPs$&Y7|Wp7(*%xlf!Lq z%{|70k8o>gk3WmwPq`2>y=*M9Pu!wWadBSg#4B&&Ld?#pnxD36$fVFzy_Q?Q$~*>j zdVh^{nU!>?!`=GuxMM%(zoE1Xp8kTM8c!P`kVxrJnGuOZ_r?S(`n9X0vJ0Z{ho0YeiwO8Sd26MF^dW!_Go= zhMxV_jV|#@w${%!7xVMK3+!|=N$uUG%P$bGC41=9^mu}8C0($DFAHrZCY4VXsmp`wenG$VqUoz8wk zWNI;6l}Ow0*laHx`e?{NC!)O?Y}?e{#ZcNC@0qHvERmtL*Kp?R7Ri{(-TtHcx*4k%v3 zl*_Ox1z4|;``|8!Re9b+@6{GW@`%u7Id(dRG;7Yn)@dy<$25FI!h0uTEUq3`JuihN zQ-=Wvt6K@;WUqc(4|aKo!y|e!7x{5S%P9%s5AH~x$7%#_Zg$Zkg*SK=Q^k%@uW>Kn z-U+DqX|&Qjhv6^b^-Xq`1mlNgs>5CV^-ovkA1XR2{hWk0odd+(WWD`4_5SoXm_ibF z9hdehX3;c!PCC_W+OJj#2`e2+el00>ti1=akFx7q5!}Vf&0KxDMEcA&9iTTz4gisA z31#Bn7i49kPjl?$=o1)~-PM5co3XJxyF)c(Lq{GL4EgL@0@kL6%!lH%&RRL4GC250 zZaVTR40U~>Flm;Mu{ienLzlsm>3JL<7>nJqPe^_r`PGjl zHZ*P&D*Bd}_G?8i$oim*EW$lz|Dr3YJkXYb@;^s?+894ogL{5^^_7?9dExADi;_{J zMySftB?w<P^0GGY$~e~m^yPWYRwEoI=QG~)b%_X z4;Q<*7BHtk>N>b@Z@Db94_~W_i*T)2HWtyP?g5&^5G?(D!JB*0{QK+plJQ=TSETlx zvn`iDSqU0QIDcFYS;XR2D41Jq9xy`#3m(AV1DQ>B0?06#*8LavG>bDCGm9$t#!Xe+ zE|*3nkub(_mkmf)r)Y}syrU#=IiAgtf8j!CNTy|sRpK{lKvah9&T57dcBUF85pPwwqRI2qN4#Ii$F>~Kfqd60FQXL$?lf&qPJ}^MX`g;MXpWE#aNL>3!=o5? z+^w8sl?yph%DmT42cp?Y6$({GV$@W%lTrsZr_AP z0{)jv^~8htaLqiOPw|=v`ju}Xb)A@;$r|dwwfJTU7N(c-g7IV-B)~Nk+s_vf8x`k(+`DC^zo+xF6w-NWDc{{>?d|!K<2VPCOYXG;t$?=LbUSvK=2yS>2L=PR}x1 zg{)1FrUM00kutx%4QCSqbFENPG+>2}YmgOwV)2pYM*(#rUJ)ff!GO+1q|eST34tIX zaa%isr)_v3|Naj()HjNSg)(~pgB(K~y2SPK>ZuK9RS!Y6eaw4ec&{U=IVcY>42(9O z9IYT+&!n0eQTLEUqPCB{H)Xj=IpG;yTis4=Z&NUQ>7U7!E~D2j~24ii!6Ws zqRBa8|2nhi!D{mw?kaozTDs}?0(Gi7ttLl9k$bW4q+`G5c%h%^1Kis#ei=+SY8HrK`!k^(Ee`&+N*dh(8AY9l`~nN|{7B%=AL5L^7;yKv8L_hdOekIituK;Q|dBA49mos9HzAo?Hw^qxp(X zF3G4IZ7vtT&~2+v&|&CXnlRObxG?63sEDh@0Vsub)X2-je|b%a_2)c-5Yy7$w1$z> z%aezc)E_G|v5#GtNGFacX&nT$bhh773bem|{yD-d5TH&S3>c>E8^LI8NMt~&zoK}O zI9K^5LGssnE7a&b{HjJ{mtypQccU+0ZFvP>hEHYKpc7dLW>LAmh}wo^TuZYHOwiVZ zU(Kw6CKUh}?*Cr<)NGNsu~=L6K@TYOb5QW&qLGAt;3=xk{_8%f$Nqi{%Jqb`!}s1T9#(LZUo0F@6QI+%n`9TpLBC8MLo{Y8LDga zb#;CejOSMt2uEXlgf^Z0H$&kpCmJEbIy5`LK-Z(siuGbnXx_`9%c8j8ZCBLbuYz!D zwhdOJ>9+cTVq37Jt4C<9r+2+0qtz^nU7vW|42`QQFZd@Vpn(jVPdZ7JOP2374n^Tb$~EmRyn)0~xI1N5VmK2%ADcZVv>TI~XtN<)$2(V4;w(Xo-q)3c$cMMhrvCr82%18q)fQ++O@ zxQF0iUkli`ilhoLV;Xq-o&R_M3#9=PC>+nlA6Z^+RH5)+Yv(l*nJCvf%Dqq+RRltT`|lzD`{sbZwD*RmHfzFMiVJ$|OIN-_&g`&(FWetP$WTjC z!WXDvqCv<3bwFh`;)XO%5^)wQ*NcbSg4zWT24ioW&Hz7+p)cU{R!)>kD0tmu$R0ul zhNaeK@TTDrB_NuI3TLD!a3`K?X&b+;&7~K2V4m(cdA)d}esVZ+6nBSjNS|&a1ch#J z?n|D)d`?q{js8-GcL-r-KS`@9F$`pIF7&%48(&g16k2D=rky+i@}iDJ^h9{KFBS{4 zNz^i)7$7C;fz;!GRBO5iJw9uWEL+?+& zC1hFowjO6=NNnO4C0rVoFg09$Nx7<4;sLg_ym$UPy@p-#Pk87Hjp+q+Q_a&66k6tb zK9L3^ycR{?6|gf>JSTtcj>W898wUb(hZlslCDl%<$6ipjdp#dMD(_sfYsfx^No|up zvehnH^%@tOKR6!jjz)i^q1H#hVxWeO)z5)YQhl;K<(do1G4muoEx@nOO3&`6K1VP} zB=#^N&!2+)@{%N_KC?-Gh#48d5*`HhBvr77{9E0~>Acxn+1fP2q2f7ht|_2Osx5ci zu10_YVyafyvrm*dmNx=zT-eaW!qgR{ia)>E7@OZNjkj!ftWD)O7-N5tAP@zYA{bJh zbtQAE7R;g1_AYNBk*;E5e%ce+$V%g00#R&YCTs>My)kUa!TkDsq&C%dU3tBzOvSA` zZoqmTvWu~GS(NVTFX1Z@nW2j(0cOC7cqI&T&9X*bNMVPO47M^V*H&j`)d?P4h+z-6%_jC{FZ%N^?Vsb6>eHG9V1ryjowQ_9+ zsChzKr}#>4Xge9TbjGa7TxQ4uLl%sIOrcXV?F0LxT~x; z6`$ygU0OG^Hs{gZFG)n^b1Z`l+U&P{#l>A?81-K931_9HSibO&A^k6s*$~0{CV$a0 zc;8H5q$$-n0du24=H1m56$HmzMM=}($^!*t3jHWMhnDbPe_Wqw1wb&ABBNPk!Os;X zd3w5sMpTrn`n%Ln#~V{+I;yvgXWu6n$YestGaA=(T6}leX_v~28B*$)0eFb%&9@;r z{ORolxqvYAHw17_^k2)u{@aJf^|s7Hg>O|AIewW2oBp<2ZJ}gSxmgalnF+}9 znp*4HPKkbvs42j&Bh`A%OkqYbFX5UBU%Uc(QqqmJ%LWzE&gmg!XsQopMLqc?Iuo4C z%?^0tbM;Ybs_vT9!?@}^xo`FPJ$)5qg&7B!p?U)l!(W7O7%0Il{H)lAG112F4;~D` zOg!fBM{DW&=Cpv(cvxWBju4_3eZ7HyY-g*YmwQtpZmbFxav9at_)e05cwDC+J#i8rMstI0X3xAW>?c$bso1M1b_AK-5CywDU=`260p zCe#cXvbjK_=4phtJ=s4QA<)gz_4r+NYAm~?=N-f!(oNw}4a0wPfyMW^5LcsQ;o>mt zP-xqQdX%~UNJ#f!AH*XsuH-@f@peh^UDrC}kzUsJ)?4*~al_NbTYkdNUG`+xOP*pS z6`PSifgSdf$|~FBSh#O^=+KTBA3M?<5SfgB;mkph2rP)&Tyw57JUUgt#G)CF^|E)dl1?u&*?*QUKU${gsgIi@bb-Z;d*UCtuYD#z zJ5xl%3`Ci$3<=L9USMor$6}pFs^P9uI2zM>_1g~AVPu6H2(~xxSv!$!b>?Qn zO+q-Ar*E}kezf8{^W7z$v+(3u9K)`z{Dk$G)Ynrzk;7&2)1K?NFqwE~$ekDq$1Wlj zljPM>HdMnApL-|LcX~P@=7kURUEXlll@lB5z6&dOSX~7xle!X74_8Z}HY+J{m zmAJMx|Dy7(vb@LPr^Vx8d+Prd*Pb9V(#R+|_+@|mih-7(TPH54cQ;IPx?)uNNu z5DZk||DX}T!+UEbAJJtMlH{|uI_Tan`P9X{%ZDT04xm+KSnG?&@LT?N{-i#kC-AaI zW&S2rPo{0(YG@Ej;WDB8r?bx?0_kd++Ie+;+5tjfpAtXeSN^=CkKa4O{XPnjd0{d9U zCSPs+pIto}ERQaac;yzea zY#zAcEt*M4DH(XC^hfgf1bd$#k;;;Cj8gjXD~VBAbc1CYlqHm7g}Zyb zT~ta&OY=<{T?axrn0C**H>&l>QZ)}6F$RDo<{66fkq9L$NQc>h3sBJpvWZ5F_SL)Az2p)VOe>p?rLX$~?|~#Quk4)b zl6WA?#c1QrbP|_ZC}kseWJM*cMjQ)oY&PguP{qtVO)KKd`rVfxl=2?-TuU@rT&~Tz zHT@mizo%+UFNR@+A?B%0K-*{9FF*c#Wuwo%$sN48S-Sdht)vNR$^jwqAj`fdXV(z4&xGn zX{EI1MjZ4tQt}suAS;?#Oz5*0%n^EI=rvJn=3(F?fLdAu!~^VfCPzo^oU9WVY>HgK zGz!4H@=Ex97p~8|oXsp{JOta>8=p2w>-)`N&rl2(qk*gW!FY^P@v!{8)OHcgo`%-=(Pb-H3DD=UmQt8;U8oBu0Jrqv$ z6@;#(kP3Uew?2Sz`^F z?F}lubZ#fE__u$ce-X3_qan?+5B$pnaj(u*3L|lt3Kj(t(S2+wyyiV#GlajxJV&tmo3c5Lps#}F<*+#Ny`4Wd zKOFS1e+K_7dQ|A#=VN-$f;<&?3qc}q$7W(Emfq79n8EyvV{r5ZaQx~+M27KvW7`+u zJ_lAHMqD0eKGK^mhPkp}Kx-QEfQ#ktzHHp`2FysoI~1toDS!$Ed=K)<9){G9CJgr)bcN8}CiJhMJF-qRAO#X!P;P22})~2kOv5_R`HDO)n!9AvD zB##D#^L97f0>~>hpQ(g~oDCA?{&lpwz<;*0PWaop$x439p2O?;Tx4r1pq3_u?4MNq5wnzc5ar^^lyi-C1>;hpa4Xm|z;(`P+rqA`NF-G@+zK z(h&}16ato~_ebGY`l?UQx8ciR;1VP_@!YS^7c1oFWjv}J5n{hG6yY>x%e1xXy# zh`zf~07BpY=IzEIO0oeQJ;Q_?@5jz7(;KNtbgEnIq`5Smw~-3? zZnx>xwIpS;H=TKQQJZtGp-7sQO4 zNOg>N|Ly?dhhDv?Vwu}u+>ygjWTQCGy?z!Uj+!yU5SK$dXr4}uRIwYx;q@QQ>-u9K zrZTyJjt{f-QoWnmOhL*~++q0~&%@5oO}|IHeaVf^i;uTo9_6$3N3CXp$Xl{=W%@l= z3m2lxaZjvi^Q=FTU7LKcBw}Na1qzJ!;gT)~1*2(+B*ioMf3-E)mY}yJDlzYN({0$U z?0#pc>a0WOVrR$#pe7)KHVbJ5z^1n_RK-K+EeBM?#S=Srnu6;!z66 z6c&kYgEB_|cFsN{fY_Gsi86INw}c)*-acqe5Gb4IT3wA-jOMQUYHb(;wMMPUtpAuT zaD8xn*R5G(MQ1K!PVs7cL#f-4kIX1_bhC~9-kgp+cb>BRLvP_YOsTD;j6+*D8Z_|x zLXVLMgvJYM={fQs%40m1pbE!fY++@Ulrp413%KtN2c`MdAF~c>Ww`OeYUr>C#z7B` zobPH(Z&ex|y4fqa_nzP*trM%@9%*%1?JS#(L7-Aw7H%hk?kE@3Yi&?XJFm^s;=By( zAhFUKa;8$M)>i+%NMnF%cP(i`pXlagFiE8|LvQ#)s&LSp^Z}YtHzN4^))p0>B?wwX z1q)OK463kAe*e;}cRZ0ir1+>GRIy0wTX*(smf#zsa`P#norFL^vT?NWZ-3g-Y4yxE z{Wj+YD>5QBuQWsQ=$o)U|GUgsqgwYvMl&fKTaVU$z=SA+=JfD%_!ZB>!dM^7F~_56 zt9>fJ&g-Zb`c~tfC*|1<%Bf>H-31Hr1K+W*LGgPySC;arj3&{B zvV>}_MK{`=Ap-~M>*}m1GhLH$Mr%6+dqbIN^Uf~}ox@w_7|bz}wTl38{JxK;fBCNY z2W!3)pLhEo=TcsGKwNw8lk+R#RqY{!C$n!bGmAHNakDF|{tI{Ye)-{$%gc!&FqDUKO6JIKu}FYvkbYQ|DR_HcI3=XaHR_>57j!`V zH5q7gZ%cf3a1h>+;XTSBky`01!?-jB*`UxOiMz>{G69qtqVWCbXv+wYFoXZ(#F2b? zJ802`VhF2a8?{AluUXCamQ@Fi7?aMv@~aGUJNb`cj$v>PoMclU>>{;JgS3m-t&vEB z(#gU!WjLC05R{>f(8)dcyx9DWN0!>C&qO9td+orfeSOYNP1p2+fG*&gY2{vo3r{;2 z*y^OBlpLOnWNt>ALIpEVKD~x}f3`MPSgK9CJDkf9@2ge;AF#vTTh=3~(u{Ey8Ch3J z-DN8R*7I+;H+VX)lBvy7M1WR9Tdr@8zjqB~Upx?gtP%%J4QQA1v5JiGbZ`j zYEI4)ob&EO(rswCaa64tTF~h zg?)8qzyX{kHRZ}7mcUizR<4}{cz@PB%X02})WsSJaDzzf$cX5qi*q_`Lpv_lr)9lB zTm6I&beG<9oA-YGe2%m<>5~sRO)G20j0YnnjjbK>%8}^UmvlAYt066PhJj4{xzm-?kZ`=zL-7^qA~E++cm3?eE}sz ze0%X;&;)e-zOMV)K!{1E&2fm<#6>F-4l7`6_l8zAp+m^qnovwIg6wA-3MK99%Ri|QYkCau#0$GqktsOP6@n_1_-V0Nkv$~swN7B!Tfmn8)&s2cTg z^-ys8MJUs%zhd0wAi;dSJ`+kOp&YwDC$>1y>-hA9*xHn1a4o0jFISMYKAK%B@ncG`C4hAT*IF&br-465)?17wWNys?-%ZlTd zXM&5+7YYKJy_qLR)1SeR-p^tJ#&gf$U41TM|8R#f8L{_NchKl?rUxZ#2nYxi`W86o z#|#WoOr#lX8jEJ~z;z-+>|mF&6bbdN(=lUwA7bNEt%R`uCuwprVR%YX zN3GkNLmMZ%ZwavEDoM^8oQ zpUApl0^G%*{KcBN7#-TEaJ?}{4gvQJr~FlK91b!?z!>_&&+$K^>R$V>!r?uUC*wtp zkpzLgG>>1&Zgasdozmd`#3()z=)Vf<%iv0LjcNv}#0l)1!v%VE2;u!GhGS+bTG_3I z3?Qaqb0Va%Dn);6h)80ytG72?Q&t!x4|v+S;YK??5HYJ5VaSo@Vi)xCX!ddPvKB^; zi3T6z#n6zzie7m52M@v0?BG8U=6U2N^ty|a0)jdLFfU{J^h%Ujr#Q7buh`rg**mH0 zNUW0lsO5R~?DsMoe}zfm9rC9Bg%$=84>3k`e_(T*u=wA{$foJFSikpnB?XcFRw|&Z zUY)PC4wrN)kJk7XYo?7tnqJF^jtoUy0F*+J>pqi3Rvf&zNyxX? z33`hyp`> zOPXI>y5rB^YH8*c$EJ2tta%JGxKQ*0C-GTkxHKPzaeE1zpy54u`$2BiLH+1}5%@ES zBh$8(oM<4GssZN6PtgH#YoyC`p0RVcja_C(tu_Hu|9%eQ?{C2d29V&5*f(6 z-Q)3@#08;_&5D&{ zw{jdXwSp95*BIX+WG!dUxm_Lh9L0p>WpRO!uP3h<2COcdCH0piD`na*E7zve?R06HKN&T znd^!-k-&pdpLPKw^PazCH4fgw>36E~>q7jr2KW|2b$ z87$0CH^86h-Nc)H)s6&)i_uQJI&5$bHA^MeTzmG{y!^GV{3V;@lnzuZ_i+*dh3^i) z_DOSGa1Hjk@3D5z;ob-&ROoDc?Y$Ymx4ZOUA=lYkT|-Q~9^VL(_}Hu~f?LH4$-Ni9 zn0>J@3dpDBetEs9-*Hb_sGuVuUXI)e%-0BmOE#N{41X7CJ~phseuyQZWllAi{1>=o zWctL9Z}cijnMIVF%FI~nz|CyaHFo2TQb_v!j0Ng?O^e@gN57lJWb9jt3NffqPjN|dhBLz5p3_gGmI!5>}Vt3L}qc! zDdnyGU}0X^^{n_}5+RB3P;ECqqN9UtfXMhaU+N9dI0ai9bjQ2X?;I1NN+@D6|$av46v-+6Yr- z{UAE?s)&CPH%qh$7FbZK;{49w`205*H|NXPcM@{<9^^DV4q!aJAb$@oZvUA8f5M+H zvN?a0F5V81o^k`EL^<42$IFlw$6LS+3|DG1IjckIkgddzt9=d5m$^Z;)~@qvOfT{E zSK;m8p!XkpcaVM>$hUgN8#x~HbEtz{=fb^lYp7bBw%6eWah(KOJ8YF?uoh~omGpRj z2l_y1-7jWsb42vaFyTBAxNyPN0YYv6|FcMGKcvVmKcMMX!HF!G;chE zTIR;XS8z)GE@oX9SjsrJ61NkGdh^Xw7+suHmw0|}h-syfYrzjFi;$`%Nm&8!OmCUU z@)3kTXq;CIPFy)T)J}XIEyN{STEl(K#jU8_*5aXka%;*rr<)3g6a1T>MP%iX@F+|j z+_)7@de>9K!r27y2Rb;i-d?7Ay+)|KX{0-I93qRVSeQ2u!BUT*04?NmWmnr1kf zYti|QT^q%t9Z{n2{RktpqON1%F1?6@Kfg85-$?fibonb!?3(?#x7vPjLu~9D zXJPJG5TJebi(hSDZ|PGYv7&PIg-dhAIi_I-lG4OLde5KaO?xfb^OKM*`uD;qic9F? zX=OoCV-^70izI~Zz6N9FfXww0J9jPfp0znJItNbCnR@8*g{w15{XLB#vlM0aO1OG8 z;G_{9fTBZJ@zF_*I&hvg`O9S*aV|UcXY{TQ-6PCo+1p>6`6{NNGs;z6<55x1`gel_ z8QAjwId5zfP+DEZ+C`=>+AtYagx*Uxlw%v!t4N_^Fp&nAH!VtHA)0b`UDlq)$9>{o zQiYA#8$8kcO3c$+nfs08ps8_@b{15^6zGwzCd?eJP*F#89G|j{(ss@EuQyV|QV%3G zjWt$MNh{U2^`ECD)W}uOEaHbaiR`t?gN#ZNOSCd$Hd-FfE=*snJ%yR2W#T5`1B`2e z&E9nzP5))nB8tL>qVuR8r*0Ip;?QH6*C{*8gpwYs-iafTwL|S2gxson!V!`?4MquX zhF7Dad!R>jHso#N--^b6tlZZv&?G6@Z^jz&d%nJnN4T$*uy6L{S+C}R?dAfTki*L? zhfRk>3_1WgDYMX~KLj!dE)&m)DRZ$w@+CHO(=;sAYEniIvw^K^8tSRl;tby%dil0t z=mV2V<#0RT+VW=Db`a*bzoaX6cioIU# z%om*UBl7vGC!FLL2x_el;(0G4Ltlvd`!ydH4R_LDLKR5zxwnJLtmT zH+>Z_KoBucfCJqXgDJ{ztCC6d3k#BlXn-X z-U31OeSi!>50iqVHytD|D!NXIyT*32nt|_N7Ade&#(i^C#|*Z{wQ=dZZTQqJFKn~i zi1}bVwJ#jbot(pjnURTwz`J}383x$2ap0`L>2fQ_J`VOKDwn}w!hb`j%BAlxyvB%M za{D=T#I)d%l)5e~+4rHfH|HBCQQ*?ft@q^5gDjLxLzi65(g@&DO|BQKrtYgy{E|U% zRqc>Pl%|6e;UnooPuJ_dfbmchMGhw^X>Apm(eb+7{hBvt@=tO)bZHr|i`E342WVx9 z-`=Th>QWGAue{Bkwp3NB4=hDK`(r0iP*>j=ZQ;G|)K>GNE5Zx(OMD$a(MjUBfBsh4&<2%-kQ*cc?eitOF#ckaWv#IyT4Ds$Gjo1s zy!QrpUvjBPRo@6ds4`kIrQt{MYFkp(21TAD~T0C=ty_hv;>*E=4Nth)>Fm#AzU%rk9lWVgv|0ldj!EQ<+ z8$ui+9@M^7^ls{wxW^u6&ack@iCG0F8}R^M1y)X|7Lb%fF6)z=w0q7m@CMB?2-bp) zPi@0sVV|xp#g6QK9=DDjQMQ^fc%-iTdbzp!n>a?%3$jB3V9|w{@3O8yosYHOL^TC; zscS^we_d)%%S)AVuu$vO7ag8z6BU3OoYDDsUD$|8tr9ND3J%4`=brC>-1>n90ybL`JJv}9x-(sN$fnbkL7L*xr8--E_OL>-2~s7zWGup9ePZv1`=>vG zLR0sw!lU7!k?@E=Xk_goliBI4VcFAJgGg$bpPn8}t&A_)s^oaF~v(=Nl@lQ&1}<8mKC688im6gYuPRQ%waTHjyXPcW3o}O#Lxjt}kJSA|;xh z`7;yAhE`k;TH-w{g{c2zSMJ5QK8OW8z!5y2>1?D&PpYQNu5gKqy7;@Qk#B}JK@dC*#D{zw_S`y^d{@q*)W*&VjjGTAl{yucWN zm9Mfsr-*+Gt~E@2rbv0=p+!kO;At-gsLO{x^t%4x!~F#k_KnvxrjZ8UnL-ZWXT>vI ztC684D90o?IXUclY(}+GB8&AQ&rBPm0SX&OC&H{mCM};}-$_Oxn?b{3Ud@5}IYx8< z)_e9>H&awYo~N&jME`0F0?cT%l-@oBwVlnX-fs?e*3IGx|5lt_g8DE>n!Pkz0D=?O9+0SoAO zGdk1H8MZ0=Yf}MM%=4lRr7z`>2aCt*7syCPZK!kx{Sn#=H*T->9-4iiqHHN)DnkM& zrL~TKC~1n**GiDCN_Zdd0P%G^9!r>Vc&Vv}GNEQ=6hB7UQot~T1%zN{Bs~X_3Eb@( ze&`b1x}34*)e7#YA){N4=S&`pCeH+com^Xjo#%;iKK-u%Srn%07mwZXH2Nk42m!+p z5LuLcL9bc{dbC*RsX^xb2>Sn7J^Fe#000ynNklWt?#;I7mst$ znHQ*$k%WK+5P;wM6?&@CPqcDq4tld&Rmv67$@)dj;+i=s%jHFOT}YNAwW(p#Ke2BC zAB+#@1#)C0A&`3nPOe&9RjsEQJTO#93eD0$v>wE;eF{tPYxLB|39@t}Sq?`3oG!6(M`?rr zA)qS)ogshdDE_Lit5gW%u?l>m?*+Jcovl*#J`5jJ!JqzZmE|0nUDuv$93jj!-iKF* zAI7ign-Cxb3`3v=Gr$N42qCEvZ^c%2^O|enB*+H(MB+H|+%VGI)0I|sqV33Z4 zfGrWgySWe*{-2S$XF;^(*v95b9yHuY{j4n8P?F__x2$RD!={WE;#BRgpp|hW-3S3o zA%Hm$PO^qfWXBE7f@o)z21_3@TEDoSR{J)bWVyk7Tqh>QxTEq8s{ugz5CRTC;CO7~ z{b{3Zi4!fn2mYiwD zhDw}hB}tl%`IhZ!>O*h)2JC)&t(r1U$ZP29MLK{gmsHLG;qqU#Po~R5>4X3wpbi1_ zE_*~tJgR9$Q8A8tqvkCnVbs zRXvlM8=4>U`n{*%HR47zRJ%PPB&`Vn>mUH4y~0dwG!m@`I{vQurs_|ascWw4%E@p+ zDq}HdKSE;C&A9h1eDc=aBNaB3MhK`u0K*0+nZ2Y{3HXg-dft(=sFE(rlbKmh_~_hz|Dyhn>SQXcM9O%U;;FgNqO(^rLA zrYD>H>{Q-Zd8X(RS0X)c#%_`^pld=vBLc0M^Ej4aNKRvU&BdygRGkQtZ-jseDuirC zkelg71i4Y6gJD@MOR{l`c_3i?LN?&~WAQZlCIkq9%m@gQFbiG=f6AQ8n7fCGwgu8h ztEk~QEW^K;uM$;J#uiyEGhC1kt)_;i4fs|51l&ZfL>@J-BI!g@Iw9bA1hAC#OSv+V zj|YR~`QoE)6*`=cZN~XNVV3Xssj#U(t4nQxoaAq0a_gqne@r}U;{Bpq?12uS!pENz zU8qt*z#a&kG`42!Js&iE5XyzRtxc`FCQX^N4Rv3Nk5ZobmhX2^A;&H0FwNl06e2TiKw>V9HDgXh*>95j^kX@8xgWMPyeJb#I?I zW#Zcyqg|jv(x)fYOxgpI&Y9jSbXAnqmIU=)%X__Oz&}o^n)FL-i##5Uc$y+>DH9=( zM+m?{XH46sww%UvM*Xi(oI0@sf1}* zL9xvZo1d+%uiX$1g})@r@(=NfGG2oiDw10SI#B&uY+dr89FsjUA?6iCp$O*%2u9+? z4{0fSXI`{zA=ajsdO0NDk?y9+=`xnke>EHok43%bE2#UY{X45D$Y}K}juVNjqvq!mQHy>*~B}qpKZztEZ zG_>5Cly2)w$CGTlb*3zx@)7LLuuKpHL$*(j*F)cm5!jF3)>?1KdyiQ?*HoHeb<#Y% z?ff>?J-emh*}m7~^W4x_-x##I?&sWm&ZKB8%2$QOm)}P4L-#o>WLRf)-PP)sHzXSe zrgCXz4Q4HQ!0_sL8hx9Jz<%^rR`~+Hdq4(iRs`v;WJP^Ni7V*h6Q%J#blvMPN#K5r zRey=kNtjfhF4Z8^;pr^KHe!vJh;jG1O}95WY7s&lq`V{9xZyQRYv$vRox7Es6i=gX zLlLkHxyG%8TM>UP{YL!Srr&?Y64oyuPQfla>R+|Esv0EQhaT*!cGO>q&Us6+92|He zxI=xRui-U|kJ<8BPdfBRz;fiuP2_*v7$(?+KXAML4MfpN_+Cq(WkoKcZR!7*I9H-w zA23~81Y}zTx39%~Mizp(8xS4YBk&_*?Pfd$!vEo)l(l$J9y zAHrwe0ym)_m|3Tebv=gRfp3_#S)z`5=dCCr*%VD*xv26~QS6@_yD-IVN=ku%9mtid zU9+g>WP|~H4X1Y7;;0)v@2_K**T+-9Pe~R=fJin&<5VxMJ_o(BRmj3)BhZlo0lSdv zVbujpb~WPsoD`;5IVl>8#Xh=a<(9XstR(4{8UZ5N)VL(33V6M&!|UZTBp#o5BcIQ0 zMy~x$RlT(Os~C6ZfuZeo{26Qir`t_w()SPoPG7sNM?1?{J@Z6|zvEQ&YQGJE`}GiN zV;QY@ZN0jzr|eiPeOznRao1#g&${uf@gP{fXPo^GnLTYkrAgyB0*(q06F16;?>Fk- z2t)2&<%`RI?ic+_Fmrw_u6dvls~o#PB5&Y_z>DvJASzI<5TAH(lx%e6va%AB65TP4Hswx{_sBY+!Nsn5L@nW6xdd{^V}Ie z7+$yS$J-*A%;By}W7@|Q^m+S=KssJYluEEt$cJ_0fv6lU#F7%049fgq9m5^ zh*wD4fu8&$b|lyNtEL?jk=V-p2ZD8(u(9ktnf)DsUdk=2A1O900=XlHVO6t71_Yov z!!5ZRFh+Tvi5ka1eoukh*ETO}=Ig)dNJ8M^wbRG>T;k7*i~O^8?C21B`=UuXY;q~H z?}220WD{*k%@ha_$)*5BOH$QMRnvu-umXKrzVDb($3Y1GvD@Xo_pi78wbiIf>@4?! zWm85J78X2QSnR#nu;ZH;+x#vFw@j;O5bGZi zO!`g)&v+g(WUX?XT<)p|%RT#pqwX(qOJDSi@_7<%-M6>bz68q+5y-0`u|4g2PCZ!z zTW-pdDI-9VH=~Mg;^R|TRWHVOAH^ToomSWYPod7HXRF6s!@ow%yrZzt`*p9+H3Zuz zX1m^c`yk`JS-Tzx$vM>t8wuJKf4%f3lGO{z405V(u095FGjBs*ln*7O!@L#u?~%ju zPvLudAcMHPzcqjD%-f0zyw`aHF1~+P%41hsCu{HEUU#-V_BQ*Q$1}Vg+13J5ydQyd zfA9p40SHvySb1iDOay@?uHI?Eb|Gs$0ncv{#M-7);0Az|Lgd1=GZzO6+~4&DJQ??F z>+6lMci-i^+SvDiANE7ohUF{vwSY8_BS2%aaU@Lm-n?@2-%-sB#4?=?2lIJw!+MBs zXKq5(wyt9S9n-&2P~^MKA8;!g-~MoSkFD+H%HxISceXcz4CqIIdb0E&SQdc=t7m@R zU*ujBDDaF{s-*2*wqPj6g0QF?8+ctTt*@89G=XwL)`wI&_ z^#nbCP?C}I z*aHC~S$njTBek5nX67gSUe{xVMc&hdEQ4qcC>{R zJnW#`>v^oW#5+ZBWlyegqj!H`f0wPeEzC_zX$P=y$O)0GLmJY`X7I3sewXX9qGI24 z(Ix1|t}wa-QMUSmj#vKh-B4cMkN#?PK#1L zXYVLmNr+&{HIZx*CQi82_0`P@fRGK@BT%)Zst&W{Ph>ACuOz&MCDtFwD~t2Q*eo4q z@&qL^Q!xTHi)xBNuxk_-pp5+pU@0y2VjcP{5Xm~UVRK~`g5K1l?g_c7->&syl@|qs zv1{WEqc@T4FaXhQgoZN_Ad+=vyXL|wK@z0< zQE~tNTDefbu61H~a#*%4k~zh-7Wsbh%YL5{;~d21U8m!j`pn zLF*+Q{*Kc&FWcN?OAAP8RR~ak7*%jlW{yD3(wg&SS*Crc)I1JY)&)<3N29Uml2=x~ zvRh42%4=@~h-B^Eex_?*bzN0J@CmP@;^R%P+@=+RU$-!cUDL9nWvfk1AhokVfa1w! z0bV{91;O47yNO_xHU0;P<*>MAS<7R{ZvR~#mDx)&Ol_+0aUg{OR zZz3e$_`EZIID=)@j)|>$U%+>7V|}Azw#^;RY`U);0qV&r$0~1gf?$6{1UsbpB`n7H z2{u3c-o|Ac_YS#B353951gIxVk3vQSDsQYjQ*?>1AQLTIIfB$ipm9y}?alun6J98`fKp> zJSz`PPc}J-ga$bzH#Dzl?n}N&*MvYO1ZcP*6FPZ(ulk1SvDmHazaZJNJcfb6ZS4s` z6c#n#*8F}x<3P4+M}T^=+OczXNjF;!1M`_+{|SQqHdbr>#n}x_))|ce^<<4k*oN|_ zESXY^FnllDQ1xt8-U9*8fLzy4e0$=Cb?er}vK2_>2?6TK(lam!f%2=$Js{Zo1|>TC z;w|{OKFx`Bvu|??Ci^63n@iT2jR29X*@#=bKL1Gn4IrIUEv}|^^qUFx3sVH5`^u$5rlh&4K&{g9oO?u=s@V@Z9m=?v4PH{qQem&l680k>t!Bd$DF3ueaIHUT`%xQd z#g#@BshFh@Ad6Q3lDV*dXxxLz+Zas8%-O*^cmHEBZ#=!*c6tiE`Xv=j(n z-1cY;A$$cBSMOjc>mN2P-*hlVHcBD{9ErgH2MB)pPv;i8@c;k-07*qoM6N<$g8gZJ AsQ>@~ literal 0 HcmV?d00001 From 5a7f91f6d8294edbe2afdc6c58c812476aaa6e5b Mon Sep 17 00:00:00 2001 From: Petr Jasek Date: Mon, 20 Feb 2017 10:39:23 +0100 Subject: [PATCH 135/821] fix create_index when using mongo_prefix and uris when there is `MONGO_URI` defined it will be used no matter if the resource is using prefix or not. instead of custom handling use pymongo helper. --- eve/io/mongo/mongo.py | 36 ++++-------------------------------- eve/tests/io/multi_mongo.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index fd21689be..08f68be43 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -958,38 +958,10 @@ def create_index(app, resource, name, list_of_keys, index_options): # pymongo directly. collection = app.config['SOURCES'][resource]['source'] - if 'MONGO_URI' in app.config and app.config['MONGO_URI']: - mongo_options = app.config.get('MONGO_OPTIONS', {}) - conn = pymongo.MongoClient(app.config['MONGO_URI'], **mongo_options) - db = conn.get_default_database() - else: - config_prefix = app.config['DOMAIN'][resource].get('mongo_prefix', - 'MONGO') - - def key(suffix): - return '%s_%s' % (config_prefix, suffix) - - db_name = app.config[key('DBNAME')] - - # just reproduced the same behaviour for username - # and password, the other fields come set by Eve by - # default. - username = app.config[key('USERNAME')] \ - if key('USERNAME') in app.config else None - password = app.config[key('PASSWORD')] \ - if key('PASSWORD') in app.config else None - auth_db_name = app.config[key('AUTHDBNAME')] \ - if key('AUTHDBNAME') in app.config else None - host = app.config[key('HOST')] - port = app.config[key('PORT')] - auth = (username, password) - host_and_port = '%s:%s' % (host, port) - mongo_options = app.config.get(key('OPTIONS'), {}) - conn = pymongo.MongoClient(host_and_port, **mongo_options) - db = conn[db_name] - - if any(auth): - db.authenticate(username, password, source=auth_db_name) + # get db for given prefix + px = app.config['DOMAIN'][resource].get('mongo_prefix', 'MONGO') + with app.app_context(): + db = app.data.pymongo(resource, px).db kw = copy(index_options) kw['name'] = name diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index 9e31b69d0..1974e6397 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -9,7 +9,8 @@ from eve.auth import BasicAuth from eve.tests import TestBase from eve.tests.test_settings import MONGO1_PASSWORD, MONGO1_USERNAME, \ - MONGO1_DBNAME, MONGO_DBNAME + MONGO1_DBNAME, MONGO_DBNAME, \ + MONGO_HOST, MONGO_PORT class TestMultiMongo(TestBase): @@ -186,6 +187,35 @@ def test_delete_multidb(self): self.assertEqual(lost, None) self.connection.close() + def test_create_index_with_mongo_uri_and_prefix(self): + self.app.config['MONGO_URI'] = 'mongodb://%s:%s/%s' % ( + MONGO_HOST, MONGO_PORT, MONGO_DBNAME) + self.app.config['MONGO1_URI'] = 'mongodb://%s:%s/%s' % ( + MONGO_HOST, MONGO_PORT, MONGO1_DBNAME) + settings = { + 'schema': { + 'name': {'type': 'string'}, + 'other_field': {'type': 'string'}, + 'lat_long': {'type': 'list'} + }, + 'mongo_indexes': { + 'name': [('name', 1)], + 'composed': [('name', 1), ('other_field', 1)], + 'arguments': ([('lat_long', "2d")], {"sparse": True}) + }, + 'mongo_prefix': 'MONGO1', + } + self.app.register_resource('mongodb_features', settings) + + # check if index was created using MONGO1 prefix + db = self.connection[MONGO1_DBNAME] + self.assertTrue('mongodb_features' in db.collection_names()) + coll = db['mongodb_features'] + indexes = coll.index_information() + + # at least there is an index for the _id field plus the indexes + self.assertTrue(len(indexes) > len(settings['mongo_indexes'])) + def _save_work(self): work = {'author': 'john doe', 'title': 'Eve for Dummies'} r, s = self.post('works', data=work) From 224dcebe3e94de5e56d54c1ad091a41abecb227a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Mar 2017 08:09:17 +0100 Subject: [PATCH 136/821] Changelog for #990 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index fdff2a358..402c00769 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,8 @@ Development Version 0.7.2 ~~~~~~~~~~~~~ +- Fix: When there is ``MONGO_URI`` defined it will be used no matter + if the resource is using a prefix or not (Petr Jašek). - Docs: Add code snippet with an example of how to implement a simple list of items that supports both list-level and item-level CRUD operations (John Chang). From be9186337f639c95e73af60c5e604056517a6ff7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 5 Mar 2017 18:53:08 +0100 Subject: [PATCH 137/821] Fix: consistency on validator exceptions. Validation exceptions are now returned as 'validator exception' across all methods (POST, PUT, PATCH). Closes #994. --- CHANGES | 6 ++++-- eve/methods/post.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 402c00769..a1df0b79f 100644 --- a/CHANGES +++ b/CHANGES @@ -9,8 +9,10 @@ Development Version 0.7.2 ~~~~~~~~~~~~~ -- Fix: When there is ``MONGO_URI`` defined it will be used no matter - if the resource is using a prefix or not (Petr Jašek). +- Fix: Validation exceptions are returned in ``doc_issues['validator + exception']`` across all edit methods (POST, PUT, PATCH). Closes #994. +- Fix: When there is ``MONGO_URI`` defined it will be used no matter if the + resource is using a prefix or not (Petr Jašek). - Docs: Add code snippet with an example of how to implement a simple list of items that supports both list-level and item-level CRUD operations (John Chang). diff --git a/eve/methods/post.py b/eve/methods/post.py index 7050d78fa..1f6bb212b 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -212,7 +212,7 @@ def post_internal(resource, payl=None, skip_validation=False): # validation errors added to list of document issues doc_issues = validator.errors except ValidationError as e: - doc_issues['validation exception'] = str(e) + doc_issues['validator exception'] = str(e) except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue From 6763d88dec10d93c8cfc74bcc6ab640ce85384cd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Mar 2017 09:34:13 +0100 Subject: [PATCH 138/821] Bump version to 0.7.2 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index af797d17c..b938c8fcf 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.2-dev' +__version__ = '0.7.2' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index e2c1f0529..729849486 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.2-dev', + version='0.7.2', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 2bb686abfd170b69017cbc5aa8177cc8a550a87a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Mar 2017 09:36:08 +0100 Subject: [PATCH 139/821] v0.7.2 release date --- CHANGES | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index a1df0b79f..effc2164d 100644 --- a/CHANGES +++ b/CHANGES @@ -6,9 +6,16 @@ Here you can see the full list of changes between each Eve release. Development ----------- +- *hic sunt leones*. + +Stable +------ + Version 0.7.2 ~~~~~~~~~~~~~ +Released on 6 March, 2017 + - Fix: Validation exceptions are returned in ``doc_issues['validator exception']`` across all edit methods (POST, PUT, PATCH). Closes #994. - Fix: When there is ``MONGO_URI`` defined it will be used no matter if the @@ -17,9 +24,6 @@ Version 0.7.2 items that supports both list-level and item-level CRUD operations (John Chang). -Stable ------- - Version 0.7.1 ~~~~~~~~~~~~~ From 8fc54b9d67c58bf60f10026b9debb24f8b3e8e29 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Mar 2017 11:22:56 +0100 Subject: [PATCH 140/821] How to use ``ALLOW_UNKNOWN`` to expose the whole document Addresses #995. --- CHANGES | 4 +++- docs/validation.rst | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index effc2164d..6a70beff0 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. Development ----------- -- *hic sunt leones*. +- Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose + the whole document as found in the database, with no explicit validation + schema. Addresses #995. Stable ------ diff --git a/docs/validation.rst b/docs/validation.rst index 0a55b88c6..c1c43e847 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -165,7 +165,7 @@ Consider the following domain: } } -You normally could only add (POST) or edit (PATCH) `firstnames` to the +Normally you can only add (POST) or edit (PATCH) `firstnames` to the ``/people`` endpoint. However, since ``allow_unknown`` has been enabled, even a payload like this will be accepted: @@ -180,6 +180,12 @@ a payload like this will be accepted: option is enabled, clients will be capable of actually `adding` fields via PATCH (edit). +``ALLOW_UNKNOWN`` is also useful for read-only APIs or endpoints that +need to return the whole document, as found in the underlying database. In this +scenario you don't want to bother with validation schemas. For the whole API +just set ``ALLOW_UNKNOWN`` to ``True``, then ``schema: {}`` at every endpoint. +For a single endpoint, use ``allow_unknown: True`` instead. + .. _schema_validation: Schema validation From 4485a1d57af57045397cc133b0670ced71009414 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Mar 2017 17:03:34 +0100 Subject: [PATCH 141/821] Acknoledge the move to pyeve organization --- CHANGES | 2 +- CONTRIBUTING.rst | 8 ++++---- README.rst | 6 +++--- docs/_templates/sidebarintro.html | 16 ++++++++-------- docs/_themes/flask/layout.html | 2 +- docs/authentication.rst | 6 +++--- docs/conf.py | 2 +- docs/extensions.rst | 8 ++++---- docs/features.rst | 8 ++++---- docs/index.rst | 10 +++++----- docs/install.rst | 6 +++--- docs/license.rst | 2 +- docs/quickstart.rst | 2 +- docs/snippets/index.rst | 8 ++++---- docs/support.rst | 2 +- docs/testing.rst | 2 +- docs/updates.rst | 2 +- docs/validation.rst | 2 +- eve/io/mongo/mongo.py | 2 +- eve/methods/post.py | 2 +- eve/tests/methods/common.py | 2 +- examples/notifications.py | 2 +- examples/security/bcrypt.py | 2 +- examples/security/hmac.py | 2 +- examples/security/roles.py | 2 +- examples/security/sha1-hmac.py | 2 +- examples/security/token.py | 2 +- 27 files changed, 56 insertions(+), 56 deletions(-) diff --git a/CHANGES b/CHANGES index 6a70beff0..456aef277 100644 --- a/CHANGES +++ b/CHANGES @@ -747,7 +747,7 @@ Released on 12 Jan, 2015. - Fix: PATCH and PUT don't respect flask.abort() in a pre-update event. Closes #395 (Christopher Larsen). - Fix: Validating keyschema rules would cause a TypeError since 0.4. Closes - nicolaiarocci/cerberus#48. + pyeve/cerberus#48. - Fix: Crash if client projection is not a dict #390 (Olivier Poitrey). - Fix: Server crash in case of invalid "where" syntax #386 (Olivier Poitrey). diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c4e0b6835..cdae72260 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -61,10 +61,10 @@ with it (or notice any typo and/or mistake), why not help with that? In any case, other than GitHub help_ pages, you might want to check this excellent `Effective Guide to Pull Requests`_ -.. _`the repository`: http://github.com/nicolaiarocci/eve -.. _AUTHORS: https://github.com/nicolaiarocci/eve/blob/master/AUTHORS -.. _`open issues`: https://github.com/nicolaiarocci/eve/issues -.. _`new issue`: https://github.com/nicolaiarocci/eve/issues/new +.. _`the repository`: http://github.com/pyeve/eve +.. _AUTHORS: https://github.com/pyeve/eve/blob/master/AUTHORS +.. _`open issues`: https://github.com/pyeve/eve/issues +.. _`new issue`: https://github.com/pyeve/eve/issues/new .. _GitHub: https://github.com/ .. _Fork: https://help.github.com/articles/fork-a-repo .. _`proper format`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/README.rst b/README.rst index 450b90e40..ca80ad36f 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Eve ==== -.. image:: https://secure.travis-ci.org/nicolaiarocci/eve.svg?branch=master - :target: https://secure.travis-ci.org/nicolaiarocci/eve +.. image:: https://secure.travis-ci.org/pyeve/eve.svg?branch=master + :target: https://secure.travis-ci.org/pyeve/eve Eve is an open source Python REST API framework designed for human beings. It allows to effortlessly build and deploy highly customizable, fully featured @@ -77,6 +77,6 @@ License ------- Eve is a `Nicola Iarocci`_ open source project, distributed under the `BSD license -`_. +`_. .. _`Nicola Iarocci`: http://nicolaiarocci.com diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 2a673e687..10c1a9062 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -13,23 +13,23 @@

    Other Projects

    More Nicola Iarocci projects:

    Useful Links

    You are looking at the documentation of the development version.

    diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html index a3b5480ec..1e8d4a4ce 100644 --- a/docs/_themes/flask/layout.html +++ b/docs/_themes/flask/layout.html @@ -17,7 +17,7 @@ - + Fork me on GitHub {% if pagename == 'index' %} diff --git a/docs/authentication.rst b/docs/authentication.rst index d79c1de5d..1ee357672 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -579,7 +579,7 @@ OAuth2. The snippets in this page can also be found in the `examples/security` folder of the Eve `repository`_. -.. _`repository`: https://github.com/nicolaiarocci/eve +.. _`repository`: https://github.com/pyeve/eve .. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt -.. _`Eve-OAuth2`: https://github.com/nicolaiarocci/eve-oauth2 -.. _`Flask-Sentinel`: https://github.com/nicolaiarocci/flask-sentinel +.. _`Eve-OAuth2`: https://github.com/pyeve/eve-oauth2 +.. _`Flask-Sentinel`: https://github.com/pyeve/flask-sentinel diff --git a/docs/conf.py b/docs/conf.py index e2421a793..f4245b458 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -158,7 +158,7 @@ html_theme_options = { 'logo': 'eve-sidebar.png', - 'github_user': 'nicolaiarocci', + 'github_user': 'pyeve', 'github_repo': 'eve', 'github_banner': True, 'github_banner_image': 'forkme_right_green_007200.png', diff --git a/docs/extensions.rst b/docs/extensions.rst index dadbf5d6f..612fa614a 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -146,13 +146,13 @@ Olivier Poitrey, a long time Eve contributor and sustainer. REST Layer is .. _`get in touch`: mailto:eve@nicolaiarocci.com .. _Eve-Mongoengine: https://github.com/hellerstanislav/eve-mongoengine .. _Eve-Elastic: https://github.com/petrjasek/eve-elastic -.. _Eve.NET: https://github.com/nicolaiarocci/Eve.NET +.. _Eve.NET: https://github.com/pyeve/Eve.NET .. _Eve-SQLAlchemy: https://github.com/RedTurtle/eve-sqlalchemy -.. _Eve-OAuth2: https://github.com/nicolaiarocci/eve-oauth2 -.. _Flask-Sentinel: https://github.com/nicolaiarocci/flask-sentinel +.. _Eve-OAuth2: https://github.com/pyeve/eve-oauth2 +.. _Flask-Sentinel: https://github.com/pyeve/flask-sentinel .. _Eve-Auth-JWT: https://github.com/rs/eve-auth-jwt .. _`REST Layer`: https://github.com/rs/rest-layer .. _EveGenie: https://github.com/newmediadenver/evegenie -.. _Eve-Swagger: https://github.com/nicolaiarocci/eve-swagger +.. _Eve-Swagger: https://github.com/pyeve/eve-swagger .. _`Meet Eve-Swagger`: http://nicolaiarocci.com/announcing-eve-swagger/ .. _Eve-Neo4j: https://github.com/Abraxas-Biosystems/eve-neo4j diff --git a/docs/features.rst b/docs/features.rst index 31b50d677..b0c93d0a3 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2156,20 +2156,20 @@ niceties, like a built-in development server and debugger_, integrated support for unittesting_ and an `extensive documentation`_. .. _HATEOAS: http://en.wikipedia.org/wiki/HATEOAS -.. _Cerberus: https://github.com/nicolaiarocci/cerberus +.. _Cerberus: https://github.com/pyeve/cerberus .. _REST: http://en.wikipedia.org/wiki/Representational_state_transfer .. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete .. _`CORS`: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing -.. _`PostgreSQL effort`: https://github.com/nicolaiarocci/eve/issues/17 +.. _`PostgreSQL effort`: https://github.com/pyeve/eve/issues/17 .. _Flask: http://flask.pocoo.org .. _debugger: http://flask.pocoo.org/docs/quickstart/#debug-mode .. _unittesting: http://flask.pocoo.org/docs/testing/ .. _`extensive documentation`: http://flask.pocoo.org/docs/ .. _`this`: https://speakerdeck.com/nicola/developing-restful-web-apis-with-python-flask-and-mongodb?slide=113 -.. _Events: https://github.com/nicolaiarocci/events +.. _Events: https://github.com/pyeve/events .. _`MongoDB Data Model Design`: http://docs.mongodb.org/manual/core/data-model-design .. _GridFS: http://docs.mongodb.org/manual/core/gridfs/ -.. _MediaStorage: https://github.com/nicolaiarocci/eve/blob/develop/eve/io/media.py +.. _MediaStorage: https://github.com/pyeve/eve/blob/develop/eve/io/media.py .. _`driver documentation`: http://api.mongodb.org/python/2.7rc0/api/gridfs/grid_file.html#gridfs.grid_file.GridOut .. _GeoJSON: http://geojson.org/ .. _Point: http://geojson.org/geojson-spec.html#point diff --git a/docs/index.rst b/docs/index.rst index ba8866e48..6f544fd8d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -80,11 +80,11 @@ link `_. .. _python-eve.org: http://python-eve.org -.. _`Eve Demo instructions`: http://github.com/nicolaiarocci/eve-demo#readme +.. _`Eve Demo instructions`: http://github.com/pyeve/eve-demo#readme .. _`live demo`: https://eve-demo.herokuapp.com/people -.. _`source code`: https://github.com/nicolaiarocci/eve-demo -.. _`usage examples`: https://github.com/nicolaiarocci/eve-demo#readme -.. _`client app`: https://github.com/nicolaiarocci/eve-demo-client +.. _`source code`: https://github.com/pyeve/eve-demo +.. _`usage examples`: https://github.com/pyeve/eve-demo#readme +.. _`client app`: https://github.com/pyeve/eve-demo-client .. _Postman: https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CC0QFjAA&url=https%3A%2F%2Fchrome.google.com%2Fwebstore%2Fdetail%2Fpostman-rest-client%2Ffdmmgilgnpjigdojojpjoooidkmcomcm&ei=dPQ7UpqEBISXtAbPpIGwDg&usg=AFQjCNFL71vN61QG0LKlw7VDJvIZDprjHA&bvm=bv.52434380,d.Yms .. _Flask: http://flask.pocoo.org/ @@ -92,5 +92,5 @@ link `_. .. _MongoDB: https://mongodb.org .. _Redis: http://redis.io .. _Cerberus: http://python-cerberus.org -.. _events: https://github.com/nicolaiarocci/events +.. _events: https://github.com/pyeve/events .. _extensions: http://python-eve.org/extensions diff --git a/docs/install.rst b/docs/install.rst index 5ff3d600b..0681675c3 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -14,7 +14,7 @@ Installing Eve is simple with `pip `_: Development Version -------------------- Eve is actively developed on GitHub, where the code is `always available -`_. If you want to work with the +`_. If you want to work with the development version of Eve, there are two ways: you can either let `pip` pull in the development version, or you can tell it to operate on a git checkout. Either way, virtualenv is recommended. @@ -23,7 +23,7 @@ Get the git checkout in a new virtualenv and run in development mode. .. code-block:: console - $ git clone http://github.com/nicolaiarocci/eve.git + $ git clone http://github.com/pyeve/eve.git Initialized empty Git repository in ~/dev/eve/.git/ $ cd eve @@ -49,7 +49,7 @@ To just get the development version without git, do this instead: $ . venv/bin/activate New python executable in venv/bin/python - $ pip install git+git://github.com/nicolaiarocci/eve.git + $ pip install git+git://github.com/pyeve/eve.git ... Cleaning up... diff --git a/docs/license.rst b/docs/license.rst index 78be6c338..5d3933909 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -15,4 +15,4 @@ Artwork License Eve artwork 2013 by Roberto Pasini "Kalamun" released under the `Creative Commons BY-SA`_ license. -.. _`Creative Commons BY-SA`: https://github.com/nicolaiarocci/eve/blob/master/artwork/LICENSE +.. _`Creative Commons BY-SA`: https://github.com/pyeve/eve/blob/master/artwork/LICENSE diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1c99c1f1d..c67b0714f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -168,7 +168,7 @@ Let's define a schema for our ``people`` resource. schema = { # Schema definition, based on Cerberus grammar. Check the Cerberus project - # (https://github.com/nicolaiarocci/cerberus) for details. + # (https://github.com/pyeve/cerberus) for details. 'firstname': { 'type': 'string', 'minlength': 1, diff --git a/docs/snippets/index.rst b/docs/snippets/index.rst index 44c3d77eb..0ccc7fcf1 100644 --- a/docs/snippets/index.rst +++ b/docs/snippets/index.rst @@ -27,7 +27,7 @@ source_), and then submit a `pull request`_. template -.. _template: https://raw.githubusercontent.com/nicolaiarocci/eve/master/docs/snippets/template.rst -.. _`pull request`: https://github.com/nicolaiarocci/eve/pulls -.. _`snippets folder`: https://github.com/nicolaiarocci/eve/tree/master/docs/snippets -.. _source: https://raw.githubusercontent.com/nicolaiarocci/eve/master/docs/snippets/index.rst +.. _template: https://raw.githubusercontent.com/pyeve/eve/master/docs/snippets/template.rst +.. _`pull request`: https://github.com/pyeve/eve/pulls +.. _`snippets folder`: https://github.com/pyeve/eve/tree/master/docs/snippets +.. _source: https://raw.githubusercontent.com/pyeve/eve/master/docs/snippets/index.rst diff --git a/docs/support.rst b/docs/support.rst index d35b36090..836761b67 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -14,7 +14,7 @@ File an Issue ------------- If you notice some unexpected behavior in Eve, or want to see support for a new feature, `file an issue on GitHub -`_. +`_. Send a Tweet ------------ diff --git a/docs/testing.rst b/docs/testing.rst index b249b1206..987cf565a 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -177,7 +177,7 @@ and then point your browser at ``localhost:8000``. Eve uses a customised Sphinx_ theme based on alabaster_. The easiest way to get the right version is by installing the :ref:`test_prerequisites`. -.. _`continuous integration server`: https://travis-ci.org/nicolaiarocci/eve/ +.. _`continuous integration server`: https://travis-ci.org/pyeve/eve/ .. _tox: http://tox.readthedocs.org/en/latest/ .. _Redis: http://redis.io/ .. _redispy: https://github.com/andymccurdy/redis-py diff --git a/docs/updates.rst b/docs/updates.rst index 2a0312fc7..1de0a37da 100644 --- a/docs/updates.rst +++ b/docs/updates.rst @@ -23,6 +23,6 @@ feedback. GitHub ------ Of course the best way to track the development of Eve is through -`the GitHub repo `_. +`the GitHub repo `_. .. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve diff --git a/docs/validation.rst b/docs/validation.rst index c1c43e847..53f670a26 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -204,6 +204,6 @@ There are two ways to deal with non-conforming schemas: to disable schema validation for a given endpoint. .. _Cerberus: http://python-cerberus.org -.. _`source code`: https://github.com/nicolaiarocci/eve/blob/master/eve/io/mongo/validation.py +.. _`source code`: https://github.com/pyeve/eve/blob/master/eve/io/mongo/validation.py .. _`function-based validation`: http://docs.python-cerberus.org/en/latest/customize.html#function-validator .. _`type coercion`: http://docs.python-cerberus.org/en/latest/usage.html#type-coercion diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 08f68be43..c09cefbf5 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -754,7 +754,7 @@ def try_cast(v): # Convert to unicode because ObjectId() interprets # 12-character strings (but not unicode) as binary # representations of ObjectId's. See - # https://github.com/nicolaiarocci/eve/issues/508 + # https://github.com/pyeve/eve/issues/508 try: r = ObjectId(unicode(v)) except NameError: diff --git a/eve/methods/post.py b/eve/methods/post.py index 1f6bb212b..2d6a898fe 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -60,7 +60,7 @@ def post_internal(resource, payl=None, skip_validation=False): Please be advised that in order to successfully use this option, a request context must be available. - See https://github.com/nicolaiarocci/eve/issues/74 for a + See https://github.com/pyeve/eve/issues/74 for a discussion, and a typical use case. :param skip_validation: skip payload validation before write (bool) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 51b3084ea..88ac4a63b 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -736,6 +736,6 @@ def put(self, url, data, headers=[], content_type='application/json'): class TestTickets(TestBase): def test_ticket_681(self): - # See https://github.com/nicolaiarocci/eve/issues/681 + # See https://github.com/pyeve/eve/issues/681 with self.app.test_request_context('not_an_existing_endpoint'): self.app.data.driver.db['again'] diff --git a/examples/notifications.py b/examples/notifications.py index f34aa37f3..bdccd774f 100644 --- a/examples/notifications.py +++ b/examples/notifications.py @@ -12,7 +12,7 @@ you want to inspect the `request` object you have to explicitly import it from flask. - Checkout Eve at https://github.com/nicolaiarocci/eve + Checkout Eve at https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. diff --git a/examples/security/bcrypt.py b/examples/security/bcrypt.py index 2722b1b27..671d22691 100644 --- a/examples/security/bcrypt.py +++ b/examples/security/bcrypt.py @@ -14,7 +14,7 @@ You will need to install py-bcrypt: ``pip install py-bcrypt`` - Eve @ https://github.com/nicolaiarocci/eve + Eve @ https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. diff --git a/examples/security/hmac.py b/examples/security/hmac.py index 9dd3f3cb6..fb246c374 100644 --- a/examples/security/hmac.py +++ b/examples/security/hmac.py @@ -40,7 +40,7 @@ The HMACAuth class also supports access roles. - Checkout Eve at https://github.com/nicolaiarocci/eve + Checkout Eve at https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. diff --git a/examples/security/roles.py b/examples/security/roles.py index 6213ecf42..dd479dc25 100644 --- a/examples/security/roles.py +++ b/examples/security/roles.py @@ -17,7 +17,7 @@ Since we are using werkzeug we don't need any extra import (werkzeug being one of Flask/Eve prerequisites). - Checkout Eve at https://github.com/nicolaiarocci/eve + Checkout Eve at https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. diff --git a/examples/security/sha1-hmac.py b/examples/security/sha1-hmac.py index 615147a67..e5852a15f 100644 --- a/examples/security/sha1-hmac.py +++ b/examples/security/sha1-hmac.py @@ -15,7 +15,7 @@ Since we are using werkzeug we don't need any extra import (werkzeug being one of Flask/Eve prerequisites). - Checkout Eve at https://github.com/nicolaiarocci/eve + Checkout Eve at https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. diff --git a/examples/security/token.py b/examples/security/token.py index e43b086d6..a03aea072 100644 --- a/examples/security/token.py +++ b/examples/security/token.py @@ -14,7 +14,7 @@ made explicitly public (by fiddling with some settings you can open one or more resources and/or methods to public access -see docs). - Checkout Eve at https://github.com/nicolaiarocci/eve + Checkout Eve at https://github.com/pyeve/eve This snippet by Nicola Iarocci can be used freely for anything you like. Consider it public domain. From 906e9ae868efc3f8a4f69a0589a86aa04769fbd0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 Mar 2017 08:21:20 +0100 Subject: [PATCH 142/821] Remove now ignored directory '.cache' --- .cache/v/cache/lastfailed | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .cache/v/cache/lastfailed diff --git a/.cache/v/cache/lastfailed b/.cache/v/cache/lastfailed deleted file mode 100644 index 9e26dfeeb..000000000 --- a/.cache/v/cache/lastfailed +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file From f29ca81eb0f01a19e8b8f4f340bfdfdd46bca0ed Mon Sep 17 00:00:00 2001 From: "Martin FOUS (contractor)" Date: Tue, 7 Mar 2017 18:29:10 +0100 Subject: [PATCH 143/821] Fix typos in comments --- eve/auth.py | 2 +- eve/flaskapp.py | 8 ++++---- eve/io/base.py | 4 ++-- eve/io/media.py | 2 +- eve/io/mongo/geo.py | 8 ++++---- eve/io/mongo/media.py | 2 +- eve/io/mongo/mongo.py | 6 +++--- eve/io/mongo/parser.py | 4 ++-- eve/io/mongo/validation.py | 6 +++--- eve/logging.py | 2 +- eve/methods/common.py | 18 +++++++++--------- eve/methods/delete.py | 4 ++-- eve/methods/get.py | 12 ++++++------ eve/methods/patch.py | 6 +++--- eve/methods/post.py | 12 ++++++------ eve/methods/put.py | 10 +++++----- eve/render.py | 2 +- eve/tests/methods/patch.py | 2 +- eve/utils.py | 6 +++--- eve/versioning.py | 6 +++--- examples/security/token.py | 4 ++-- 21 files changed, 63 insertions(+), 63 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index 930b10a8e..d809cc4b3 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -95,7 +95,7 @@ class BasicAuth(object): .. versionchanged:: 0.4 ensure all errors returns a parseable body #366. auth.request_auth_value replaced with getter and setter methods which - rely on flask's 'g' object, for enhanced thread-safity. + rely on flask's 'g' object, for enhanced thread-safety. .. versionchanged:: 0.1.1 auth.request_auth_value is now used to store the auth_field value. diff --git a/eve/flaskapp.py b/eve/flaskapp.py index a69cf1bd1..e1fd90751 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -94,7 +94,7 @@ class Eve(Flask, Events): .. versionchanged:: 0.2 Support for additional Flask url converters. Support for optional, custom json encoder class. - Support for endpoint-level authenticatoin classes. + Support for endpoint-level authentication classes. New method Eve.register_resource() for registering new resource after initialization of Eve object. This is needed for simpler initialization API of all ORM/ODM extensions. @@ -345,7 +345,7 @@ def _validate_resource_settings(self, resource, settings): def validate_roles(self, directive, candidate, resource): """ Validates that user role directives are syntactically and formally - adeguate. + adequate. :param directive: either 'allowed_[read_|write_]roles' or 'allow_item_[read_|write_]roles'. @@ -552,7 +552,7 @@ def _set_resource_defaults(self, resource, settings): 'resource_title', 'default_sort', 'embedded_fields'. - Support for endpoint-level authenticatoin classes. + Support for endpoint-level authentication classes. """ settings.setdefault('url', resource) settings.setdefault('resource_methods', @@ -799,7 +799,7 @@ def _add_resource_url_rules(self, resource, settings): view_func=item_endpoint, methods=settings['item_methods'] + ['OPTIONS']) if 'PATCH' in settings['item_methods']: - # support for POST with X-HTTM-Method-Override header for + # support for POST with X-HTTP-Method-Override header for # clients not supporting PATCH. Also see item_endpoint() in # endpoints.py endpoint = resource + "|item_post_override" diff --git a/eve/io/base.py b/eve/io/base.py index 12ed484e4..401f78141 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -125,7 +125,7 @@ def find(self, resource, req, sub_resource_lookup): :param req: an instance of ``eve.utils.ParsedRequest``. This contains all the constraints that must be fulfilled in order to satisfy the original request (where and sort parts, paging, - etc). Be warned that `where` and `sort` expresions will + etc). Be warned that `where` and `sort` expressions will need proper parsing, according to the syntax that you want to support with your driver. For example ``eve.io.Mongo`` supports both Python and Mongo-like query syntaxes. @@ -299,7 +299,7 @@ def is_empty(self, resource): """ Returns True if the collection is empty; False otherwise. While a user could rely on self.find() method to achieve the same result, this method can probably take advantage of specific datastore features - to provide better perfomance. + to provide better performance. Don't forget, a 'resource' could have a pre-defined filter. If that is the case, it will have to be taken into consideration when performing diff --git a/eve/io/media.py b/eve/io/media.py index b28eaf536..74d3ba79a 100644 --- a/eve/io/media.py +++ b/eve/io/media.py @@ -16,7 +16,7 @@ class MediaStorage(object): along with a set of default behaviors that all other storage systems can inherit or override as necessary. - ..versioneadded:: 0.3 + ..versionadded:: 0.3 """ def __init__(self, app=None): diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index 665025abb..af4d40429 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -16,10 +16,10 @@ def __init__(self, json): try: self['type'] = json['type'] except KeyError: - raise TypeError("Not compilant to GeoJSON") + raise TypeError("Not compliant to GeoJSON") self.update(json) if len(self.keys()) != 2: - raise TypeError("Not compilant to GeoJSON") + raise TypeError("Not compliant to GeoJSON") def _correct_position(self, position): return isinstance(position, list) and \ @@ -35,7 +35,7 @@ def __init__(self, json): self['type'] != self.__class__.__name__: raise TypeError except (KeyError, TypeError): - raise TypeError("Geometry not compilant to GeoJSON") + raise TypeError("Geometry not compliant to GeoJSON") class GeometryCollection(GeoJSON): @@ -48,7 +48,7 @@ def __init__(self, json): factory = factories[geometry["type"]] factory(geometry) except (KeyError, TypeError, AttributeError): - raise TypeError("Geometry not compilant to GeoJSON") + raise TypeError("Geometry not compliant to GeoJSON") class Point(Geometry): diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 26f538013..95e9420c8 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -67,7 +67,7 @@ def get(self, _id, resource=None): """ Returns the file given by unique id. Returns None if no file was found. - .. vesionchanged: 0.6 + .. versionchanged: 0.6 Support for _id as string. """ if isinstance(_id, str_type): diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index c09cefbf5..3ae38b88c 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -150,7 +150,7 @@ def find(self, resource, req, sub_resource_lookup): .. versionchanged:: 0.3 Support for new _mongotize() signature. - .. versionchagend:: 0.2 + .. versionchanged:: 0.2 Support for sub-resources. Support for 'default_sort'. @@ -550,7 +550,7 @@ def replace(self, resource, id_, document, original): Custom ID_FIELD lookups would fail. See #203. .. versionchanged:: 0.2 - Don't explicitly converto ID_FIELD to ObjectId anymore, so we can + Don't explicitly convert ID_FIELD to ObjectId anymore, so we can also process different types (UUIDs etc). .. versionadded:: 0.1.0 @@ -954,7 +954,7 @@ def create_index(app, resource, name, list_of_keys, index_options): .. versionadded:: 0.6 """ # it doesn't work as a typical mongodb method run in the request - # life cicle, it is just called when the app start and it uses + # life cycle, it is just called when the app start and it uses # pymongo directly. collection = app.config['SOURCES'][resource]['source'] diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index 090767d00..6b751203b 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -5,7 +5,7 @@ ~~~~~~~~~~~~~~~~~~~ This module implements a Python-to-Mongo syntax parser. Allows the MongoDB - data-layer to seamlessy respond to a Python-like query. + data-layer to seamlessly respond to a Python-like query. :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. @@ -66,7 +66,7 @@ def visit_Module(self, node): # perform the magic. self.generic_visit(node) - # if we didn't obtain a query, it is likely that an unsopported + # if we didn't obtain a query, it is likely that an unsupported # python expression has been passed. if self.mongo_query == {}: raise ParseError("Only conditional statements with boolean " diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 7c595d4fe..6a3363dca 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -84,7 +84,7 @@ def validate_replace(self, document, _id, original_document=None): case we want to perform a full :func:`validate` (the new document is to be considered a new insertion and required fields needs validation). However, like with validate_update, we also want the current _id - not to be checked when validationg 'unique' values. + not to be checked when validating 'unique' values. .. versionadded:: 0.1.0 """ @@ -133,7 +133,7 @@ def _validate_unique(self, unique, field, value): .. versionchanged:: 0.6 Validates field value uniqueness against the whole datasource, - indipendently of the request method. See #646. + independently of the request method. See #646. .. versionchanged:: 0.3 Support for new 'self._error' signature introduced with Cerberus @@ -286,7 +286,7 @@ def _validate_type_dbref(self, field, value): def _validate_readonly(self, read_only, field, value): """ .. versionchanged:: 0.5 - Not taking defaul values in consideration anymore since they are now + Not taking default values in consideration anymore since they are now being resolved after validation (#353). Consider the original value if available (#479). diff --git a/eve/logging.py b/eve/logging.py index 7b6c1a657..6c9575771 100644 --- a/eve/logging.py +++ b/eve/logging.py @@ -24,7 +24,7 @@ class RequestFilter(logging.Filter): Note that the app.logger can also be used by callback functions. - def log_a_get(resoure, request, payload): + def log_a_get(resource, request, payload): app.logger.info('we just responded to a GET request!') app = Eve() diff --git a/eve/methods/common.py b/eve/methods/common.py index 85c6b8da7..6862f9031 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -485,7 +485,7 @@ def normalize_dotted_fields(document): """ Normalizes eventual dotted fields so validation can be performed seamlessly. For example this document: - {"location.city": "a nested cisty"} + {"location.city": "a nested city"} would be normalized to: @@ -763,18 +763,18 @@ def resolve_embedded_documents(document, resource, embedded_fields): :param resource: the resource name. :param embedded_fields: the list of fields we are allowed to embed. - .. versionchagend:: 0.5 + .. versionchanged:: 0.5 Support for embedding documents located in subdocuments. Allocated two functions embedded_document and subdocuments. - .. versionchagend:: 0.4 + .. versionchanged:: 0.4 Moved parsing of embedded fields to _resolve_embedded_fields. Support for document versioning. - .. versionchagend:: 0.2 + .. versionchanged:: 0.2 Support for 'embedded_fields'. - .. versonchanged:: 0.1.1 + .. versionchanged:: 0.1.1 'collection' key has been renamed to 'resource' (data_relation). .. versionadded:: 0.1.0 @@ -899,9 +899,9 @@ def store_media_files(document, resource, original=None): .. versionadded:: 0.3 """ # TODO We're storing media files in advance, before the corresponding - # document is also stored. In the rare occurance that the subsequent + # document is also stored. In the rare occurrence that the subsequent # document update fails we should probably attempt a cleanup on the storage - # sytem. Easier said than done though. + # system. Easier said than done though. for field in resource_media_fields(document, resource): if original and field in original: # since file replacement is not supported by the media storage @@ -957,7 +957,7 @@ def resolve_sub_resource_path(document, resource): def resolve_user_restricted_access(document, resource): - """ Adds user restricted access medadata to the document if applicable. + """ Adds user restricted access metadata to the document if applicable. :param document: the document being posted or replaced :param resource: the resource to which the document belongs @@ -1068,7 +1068,7 @@ def document_link(resource, document_id, version=None): def resource_link(): """ Returns the current resource path relative to the API entry point. - Mostly going to be used by hatoeas functions when building + Mostly going to be used by hateoas functions when building document/resource links. The resource URL stored in the config settings might contain regexes and custom variable names, all of which are not needed in the response payload. diff --git a/eve/methods/delete.py b/eve/methods/delete.py index a65284c04..526202197 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -4,7 +4,7 @@ eve.methods.delete ~~~~~~~~~~~~~~~~~~ - This module imlements the DELETE method. + This module implements the DELETE method. :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. @@ -133,7 +133,7 @@ def deleteitem_internal( missing_media_fields = [f for f in media_fields if f not in original] if len(missing_media_fields): # retrieve the whole document so we have all media fields available - # Should be very a rare occurence. We can't get rid of the + # Should be very a rare occurrence. We can't get rid of the # get_document() call since it also deals with etag matching, which # is still needed. Also, this lookup should never fail. # TODO not happy with this hack. Not at all. Is there a better way? diff --git a/eve/methods/get.py b/eve/methods/get.py index 0cf4ea839..021824f35 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -48,7 +48,7 @@ def get_internal(resource, **lookup): Support for HEADER_TOTAL_COUNT returned with response header. .. versionchanged:: 0.5 - Support for customisable query parameters. + Support for customizable query parameters. .. versionchanged:: 0.4 Add pagination info whatever the HATEOAS status. @@ -77,7 +77,7 @@ def get_internal(resource, **lookup): Support for embeddable documents. .. versionchanged:: 0.0.9 - Event hooks renamed to be more robuts and consistent: 'on_getting' + Event hooks renamed to be more robust and consistent: 'on_getting' renamed to 'on_fetch'. .. versionchanged:: 0.0.8 @@ -159,7 +159,7 @@ def parse_aggregation_stage(d, key, value): response[config.ITEMS] = documents # PyMongo's CommandCursor does not return a count, so we cannot - # provide paination/total count info as we do with a normal (non-aggregate) + # provide pagination/total count info as we do with a normal (non-aggregate) # GET request. return response, None, None, 200, [] @@ -256,7 +256,7 @@ def getitem_internal(resource, **lookup): Pagination links reflect current query. (#464) .. versionchanged:: 0.4 - HATOEAS link for contains the business unit value even when + HATEOAS link for contains the business unit value even when regexes have been configured for the resource endpoint. 'on_fetched' now returns the whole response (HATEOAS metafields included.) @@ -268,7 +268,7 @@ def getitem_internal(resource, **lookup): When IF_MATCH is disabled, no etag is included in the payload. .. versionchanged:: 0.1.1 - Support for Embeded Resource Serialization. + Support for Embedded Resource Serialization. .. versionchanged:: 0.1.0 Support for optional HATEOAS. @@ -473,7 +473,7 @@ def _pagination_links(resource, req, document_count, document_id=None): Pagination links reflect current query. (#464) .. versionchanged:: 0.4 - HATOEAS link for contains the business unit value even when + HATEOAS link for contains the business unit value even when regexes have been configured for the resource endpoint. .. versionchanged:: 0.0.8 diff --git a/eve/methods/patch.py b/eve/methods/patch.py index d40db2eba..8b88438e5 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -4,7 +4,7 @@ eve.methods.patch ~~~~~~~~~~~~~~~~~ - This module imlements the PATCH method. + This module implements the PATCH method. :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. @@ -82,7 +82,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, through. Fixes #395. .. versionchanged:: 0.4 - Allow abort() to be inoked by callback functions. + Allow abort() to be invoked by callback functions. 'on_update' raised before performing the update on the database. Support for document versioning. 'on_updated' raised after performing the update on the database. @@ -120,7 +120,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, ETag is now computed without the need of an additional db lookup .. versionchanged:: 0.0.5 - Support for 'aplication/json' Content-Type. + Support for 'application/json' Content-Type. .. versionchanged:: 0.0.4 Added the ``requires_auth`` decorator. diff --git a/eve/methods/post.py b/eve/methods/post.py index 2d6a898fe..e0375ce2a 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -4,8 +4,8 @@ eve.methods.post ~~~~~~~~~~~~~~~~ - This module imlements the POST method, supported by the resources - endopints. + This module implements the POST method, supported by the resources + endpoints. :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. @@ -74,7 +74,7 @@ def post_internal(resource, payl=None, skip_validation=False): Initialize DELETED field when soft_delete is enabled. .. versionchanged:: 0.5 - Back to resolving default values after validaton as now the validator + Back to resolving default values after validation as now the validator can properly validate dependency even when some have default values. See #353. Push updates to the OpLog. @@ -96,7 +96,7 @@ def post_internal(resource, payl=None, skip_validation=False): Use the new STATUS setting. Use the new ISSUES setting. Raise 'on_pre_' event. - Explictly resolve default values instead of letting them be resolved + Explicitly resolve default values instead of letting them be resolved by common.parse. This avoids a validation error when a read-only field also has a default value. Added ``on_inserted*`` events after the database insert @@ -109,7 +109,7 @@ def post_internal(resource, payl=None, skip_validation=False): Support for optional HATEOAS. .. versionchanged: 0.0.9 - Event hooks renamed to be more robuts and consistent: 'on_posting' + Event hooks renamed to be more robust and consistent: 'on_posting' renamed to 'on_insert'. You can now pass a pre-defined custom payload to the funcion. @@ -171,7 +171,7 @@ def post_internal(resource, payl=None, skip_validation=False): payl = [payl] if not payl: - # empty bulkd insert + # empty bulk insert abort(400, description=debug_error_message( 'Empty bulk insert' )) diff --git a/eve/methods/put.py b/eve/methods/put.py index b3e3e246e..fdca5c5a9 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -4,7 +4,7 @@ eve.methods.put ~~~~~~~~~~~~~~~ - This module imlements the PUT method. + This module implements the PUT method. :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. @@ -49,7 +49,7 @@ def put_internal(resource, payload=None, concurrency_check=False, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Performs a document replacement. Updates are first validated against the resource schema. If validation - passes, the document is repalced and an OK status update is returned. + passes, the document is replaced and an OK status update is returned. If validation fails a set of validation issues is returned. :param resource: the name of the resource to which the document belongs. @@ -70,7 +70,7 @@ def put_internal(resource, payload=None, concurrency_check=False, Allow restoring soft deleted documents via PUT .. versionchanged:: 0.5 - Back to resolving default values after validaton as now the validator + Back to resolving default values after validation as now the validator can properly validate dependency even when some have default values. See #353. Original put() has been split into put() and put_internal(). @@ -81,7 +81,7 @@ def put_internal(resource, payload=None, concurrency_check=False, through. Fixes #395. .. versionchanged:: 0.4 - Allow abort() to be inoked by callback functions. + Allow abort() to be invoked by callback functions. Resolve default values before validation is performed. See #353. Raise 'on_replace' instead of 'on_insert'. The callback function gets the document (as opposed to a list of just 1 document) as an argument. @@ -97,7 +97,7 @@ def put_internal(resource, payload=None, concurrency_check=False, Use the new STATUS setting. Use the new ISSUES setting. Raise pre_ event. - explictly resolve default values instead of letting them be resolved + explicitly resolve default values instead of letting them be resolved by common.parse. This avoids a validation error when a read-only field also has a default value. diff --git a/eve/render.py b/eve/render.py index 635f7ffc9..3e3b2f8d6 100644 --- a/eve/render.py +++ b/eve/render.py @@ -156,7 +156,7 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, callback = request.args.get(jsonp_arg) rendered = "%s(%s)" % (callback, rendered) - # build the main wsgi rensponse object + # build the main wsgi response object resp = make_response(rendered, status) resp.mimetype = mime diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 1ff5bc957..b28e34948 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -86,7 +86,7 @@ def test_unique_value(self): # for the time being we are happy with testing only Eve's custom # validation. We rely on Cerberus' own test suite for other validation # unit tests. This test also makes sure that response status is - # syntatically correct in case of validation issues. + # syntactically correct in case of validation issues. # We should probably test every single case as well (seems overkill). r, status = self.patch(self.item_id_url, data={"ref": "%s" % self.alt_ref}, diff --git a/eve/utils.py b/eve/utils.py index ced087dc5..e78c916be 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -48,10 +48,10 @@ def __getattr__(self, name): class ParsedRequest(object): """ This class, by means of its attributes, describes a client request. - .. versuinchanged;; 9,5 + .. versionchanged:: 9,5 'args' keyword. - .. versonchanged:: 0.1.0 + .. versionchanged:: 0.1.0 'embedded' keyword. .. versionchanged:: 0.0.6 @@ -109,7 +109,7 @@ def parse_request(resource): Support for custom query parameters via configuration settings. Minor DRY updates. - .. versionchagend:: 0.1.0 + .. versionchanged:: 0.1.0 Support for embedded documents. .. versionchanged:: 0.0.6 diff --git a/eve/versioning.py b/eve/versioning.py index 014c8c7db..740d750f5 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -16,7 +16,7 @@ def resolve_document_version(document, resource, method, latest_doc=None): :param document: the document in question. :param resource: the resource of the request/document. - :param method: method coorsponding to the request. + :param method: method corresponding to the request. :param latest_doc: the most recent version of the document. .. versionadded:: 0.4 @@ -26,7 +26,7 @@ def resolve_document_version(document, resource, method, latest_doc=None): latest_version = app.config['LATEST_VERSION'] if resource_def['versioning'] is True: - # especially on collection endpoints, we don't to encure an extra + # especially on collection endpoints, we don't to ensure an extra # lookup if we are already pulling the latest version if method == 'GET' and latest_doc is None: if version not in document: @@ -78,7 +78,7 @@ def late_versioning_catch(document, resource): document if it is missing. Intended for PUT and PATCH. :param resource: the resource of the request/document. - :param ids: a list of id number coorsponding to the documents parameter. + :param ids: a list of id number corresponding to the documents parameter. :param document: the documents be written by POST, PUT, or PATCH. .. versionadded:: 0.4 diff --git a/examples/security/token.py b/examples/security/token.py index a03aea072..328a443ab 100644 --- a/examples/security/token.py +++ b/examples/security/token.py @@ -30,8 +30,8 @@ class TokenAuth(TokenAuth): def check_auth(self, token, allowed_roles, resource, method): """For the purpose of this example the implementation is as simple as possible. A 'real' token should probably contain a hash of the - username/password combo, which sould then validated against the account - data stored on the DB. + username/password combo, which should be then validated against the + account data stored on the DB. """ # use Eve's own db driver; no additional connections/resources are used accounts = app.data.driver.db['accounts'] From 9963b3556acf12c710d5a9bca4f9f8f3654e0f52 Mon Sep 17 00:00:00 2001 From: "Martin FOUS (contractor)" Date: Tue, 7 Mar 2017 19:09:47 +0100 Subject: [PATCH 144/821] Wrap after 80 characters --- eve/io/mongo/validation.py | 4 ++-- eve/methods/get.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 6a3363dca..89a93d2d7 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -286,8 +286,8 @@ def _validate_type_dbref(self, field, value): def _validate_readonly(self, read_only, field, value): """ .. versionchanged:: 0.5 - Not taking default values in consideration anymore since they are now - being resolved after validation (#353). + Not taking default values in consideration anymore since they are + now being resolved after validation (#353). Consider the original value if available (#479). .. versionadded:: 0.4 diff --git a/eve/methods/get.py b/eve/methods/get.py index 021824f35..415fdacc5 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -159,8 +159,8 @@ def parse_aggregation_stage(d, key, value): response[config.ITEMS] = documents # PyMongo's CommandCursor does not return a count, so we cannot - # provide pagination/total count info as we do with a normal (non-aggregate) - # GET request. + # provide pagination/total count info as we do with a normal + # (non-aggregate) GET request. return response, None, None, 200, [] From 0c2f9af77428c33563f497bdcba945cd86ed7507 Mon Sep 17 00:00:00 2001 From: "Martin FOUS (contractor)" Date: Tue, 7 Mar 2017 19:31:58 +0100 Subject: [PATCH 145/821] Remove trailing whitespace --- eve/io/mongo/validation.py | 2 +- eve/methods/get.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 89a93d2d7..bb35f06d1 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -286,7 +286,7 @@ def _validate_type_dbref(self, field, value): def _validate_readonly(self, read_only, field, value): """ .. versionchanged:: 0.5 - Not taking default values in consideration anymore since they are + Not taking default values in consideration anymore since they are now being resolved after validation (#353). Consider the original value if available (#479). diff --git a/eve/methods/get.py b/eve/methods/get.py index 415fdacc5..b47b872b4 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -159,7 +159,7 @@ def parse_aggregation_stage(d, key, value): response[config.ITEMS] = documents # PyMongo's CommandCursor does not return a count, so we cannot - # provide pagination/total count info as we do with a normal + # provide pagination/total count info as we do with a normal # (non-aggregate) GET request. return response, None, None, 200, [] From b9f9a917da355695529f6d242894dc4d53511399 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 24 Mar 2017 07:40:13 +0100 Subject: [PATCH 146/821] Changelog for #996 --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 456aef277..009df98db 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. Development ----------- +- Fix: docstrings typos (Martin Fous). - Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose the whole document as found in the database, with no explicit validation schema. Addresses #995. From 2c0af53b40ccea77c813a3cbfc14d19821bb4415 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 24 Mar 2017 08:00:56 +0100 Subject: [PATCH 147/821] Martin Fous --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8c35e2db9..c33f0f160 100644 --- a/AUTHORS +++ b/AUTHORS @@ -91,6 +91,7 @@ Patches and Contributions - Marcus Cobden - Marica Odagaki - Mario Kralj +- Martin Fous - Massimo Scamarcia - Mateusz Łoskot - Matt Creenan From 15868b19533569ebbd73f4259d416daa978c6a67 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 4 Apr 2017 10:27:44 +0200 Subject: [PATCH 148/821] Use official Alabaster theme instead of custom fork --- CHANGES | 1 + dev-requirements.txt | 3 +-- docs/conf.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 009df98db..f405a4cf7 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. Development ----------- +- Dev: use official Alabaster theme instead of custom fork. - Fix: docstrings typos (Martin Fous). - Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose the whole document as found in the database, with no explicit validation diff --git a/dev-requirements.txt b/dev-requirements.txt index e40ce90f8..0f1bef02d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -13,5 +13,4 @@ Sphinx==1.2.3 tox==2.4.1 wheel==0.24.0 testfixtures==4.1.2 --e git+https://github.com/nicolaiarocci/alabaster.git@15d190f29f86141aab202843f2bf3edfde71e56c#egg=al -sphinxcontrib-embedly +alabaster==0.7.10 diff --git a/docs/conf.py b/docs/conf.py index f4245b458..a48fbbca8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -160,8 +160,7 @@ 'logo': 'eve-sidebar.png', 'github_user': 'pyeve', 'github_repo': 'eve', - 'github_banner': True, - 'github_banner_image': 'forkme_right_green_007200.png', + 'github_banner': 'forkme_right_green_007200.png', 'show_powered_by': False, } # Additional templates that should be rendered to pages, maps page names to From 36ac66f81498d4df0cbf082b2da0f69df74b62da Mon Sep 17 00:00:00 2001 From: Luis Fernando Gomes Date: Tue, 4 Apr 2017 18:37:12 -0300 Subject: [PATCH 149/821] Add eve-healthcheck extension --- docs/extensions.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/extensions.rst b/docs/extensions.rst index 612fa614a..60b8c7be1 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -6,6 +6,7 @@ that extend Eve. This list is moderated and updated on a regular basis. If you wrote a package for Eve and want it to show up here, just `get in touch`_ and show me your tool! +- Eve-Healthcheck_ - Eve-Elastic_ - Eve-Mongoengine_ - Eve-Swagger_ @@ -20,13 +21,21 @@ show me your tool! - `REST Layer for Golang`_ +Eve-Healthcheck +--------------- + +| *by LuisComS* + +Eve-Healthcheck_ is project that servers healthcheck urls used to monitor your +Eve application. + Eve-Elastic ----------- | *by Petr Jašek* Eve-Elastic_ is an elasticsearch data layer for the Eve REST framework. -Features facets support and the generation of mapping for schema. +Features facets support and the generation of mapping for schema. Eve-Mongoengine --------------- @@ -138,6 +147,7 @@ Olivier Poitrey, a long time Eve contributor and sustainer. REST Layer is You can focus on your business logic now. +.. _Eve-Healthcheck: https://github.com/ateliedocodigo/eve-healthcheck .. _`Mocking tool for Eve APIs`: http://blog.python-eve.org/eve-mocker .. _`Auto generate API docs`: http://blog.python-eve.org/eve-docs .. _charlesflynn/eve-docs: https://github.com/charlesflynn/eve-docs From 01a3d4c8d9f2778156415d5f645f548d9621a285 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 09:37:24 +0200 Subject: [PATCH 150/821] Sort extensions and remove now obsolete Eve-Docs --- docs/extensions.rst | 116 +++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 66 deletions(-) diff --git a/docs/extensions.rst b/docs/extensions.rst index 60b8c7be1..01ee8b133 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -6,28 +6,25 @@ that extend Eve. This list is moderated and updated on a regular basis. If you wrote a package for Eve and want it to show up here, just `get in touch`_ and show me your tool! -- Eve-Healthcheck_ +- Eve-Auth-JWT_ - Eve-Elastic_ +- Eve-Healthcheck_ +- Eve-Mocker_ - Eve-Mongoengine_ -- Eve-Swagger_ -- Eve-Docs_ +- Eve-Neo4j_ +- Eve-OAuth2_ and Flask-Sentinel_ - Eve-SQLAlchemy_ +- Eve-Swagger_ - Eve.NET_ -- Eve-OAuth2_ and Flask-Sentinel_ -- Eve-Auth-JWT_ - EveGenie_ -- Eve-Mocker_ -- Eve-Neo4j_ - - `REST Layer for Golang`_ -Eve-Healthcheck ---------------- +Eve-Auth-JWT +------------ -| *by LuisComS* +| *by Olivier Poitrey* -Eve-Healthcheck_ is project that servers healthcheck urls used to monitor your -Eve application. +Eve-Auth-JWT_ is An OAuth 2 JWT token validation module for Eve. Eve-Elastic ----------- @@ -37,6 +34,23 @@ Eve-Elastic Eve-Elastic_ is an elasticsearch data layer for the Eve REST framework. Features facets support and the generation of mapping for schema. +Eve-Healthcheck +--------------- + +| *by LuisComS* + +Eve-Healthcheck_ is project that servers healthcheck urls used to monitor your +Eve application. + +Eve-Mocker +---------- +*by Thomas Sileo* + +`Eve-Mocker`_ is a mocking tool for Eve powered REST APIs, based on the +excellent HTTPretty, aimed to be used in your unit tests, when you rely on an +Eve API. Eve-Mocker has been featured on the Eve blog: `Mocking tool for Eve +APIs`_ + Eve-Mongoengine --------------- @@ -48,6 +62,29 @@ simultaneously want to use Eve, instead of writing schema again in Cerberus format (DRY!), you can use this extension, which takes your mongoengine models and auto-transforms them into Cerberus schema under the hood. +Eve-Neo4j +--------- +*by Abraxas Biosystems* + +Eve-Neo4j_ is an Eve extension aiming to enable it's users to build and +deploy highly customizable, fully featured RESTful Web Services using Neo4j +as backend. Powered by Eve, Py2neo, flask-neo4j and good intentions. + +Eve-OAuth2 +---------- +*by Nicola Iarocci* + +Eve-OAuth2_ is not an extension per-se, but rather an example of how you can +leverage Flask-Sentinel_ to protect your API endpoints with OAuth2. + +Eve-SQLAlchemy +-------------- +*by Andrew Mleczko et al.* + +Powered by Eve, SQLAlchemy and good intentions Eve-SQLALchemy_ allows to +effortlessly build and deploy highly customizable, fully featured RESTful Web +Services with SQL-based backends. + Eve-Swagger ----------- @@ -65,19 +102,6 @@ Swagger website: For more information, see also the `Meet Eve-Swagger`_ article. -Eve-Docs --------- - -| *by Charles Flynn* - -Eve-docs_ is a blueprint that generates documentation for Eve APIs in HTML and -JSON formats. Eve-docs creates the documentation from your existing Eve -configuration file, with no additional configuration required. - -.. note:: - Looks like the Eve-Docs project has been stagnant for a while. You might - want to consider Eve-Swagger_ as an alternative. - Eve.NET ------- *by Nicola Iarocci* @@ -90,28 +114,6 @@ as a portable library (PCL) and runs seamlessly on .NET4, Mono, Xamarin.iOS, Xamarin.Android, Windows Phone 8 and Windows 8. We use Eve.NET internally to power our iOS, Web and Windows applications. -Eve-SQLAlchemy --------------- -*by Andrew Mleczko et al.* - -Powered by Eve, SQLAlchemy and good intentions Eve-SQLALchemy_ allows to -effortlessly build and deploy highly customizable, fully featured RESTful Web -Services with SQL-based backends. - -Eve-OAuth2 ----------- -*by Nicola Iarocci* - -Eve-OAuth2_ is not an extension per-se, but rather an example of how you can -leverage Flask-Sentinel_ to protect your API endpoints with OAuth2. - -Eve-Auth-JWT ------------- - -| *by Olivier Poitrey* - -Eve-Auth-JWT_ is An OAuth 2 JWT token validation module for Eve. - EveGenie -------- *by Erin Corson and Matt Tucker* @@ -119,23 +121,6 @@ EveGenie EveGenie_ is a tool for generating Eve schemas. It accepts a json document of one or more resources and provides you with a starting schema definition. -Eve-Mocker ----------- -*by Thomas Sileo* - -`Eve-Mocker`_ is a mocking tool for Eve powered REST APIs, based on the -excellent HTTPretty, aimed to be used in your unit tests, when you rely on an -Eve API. Eve-Mocker has been featured on the Eve blog: `Mocking tool for Eve -APIs`_ - -Eve-Neo4j ---------- -*by Abraxas Biosystems* - -Eve-Neo4j_ is an Eve extension aiming to enable it's users to build and -deploy highly customizable, fully featured RESTful Web Services using Neo4j -as backend. Powered by Eve, Py2neo, flask-neo4j and good intentions. - REST Layer for Golang --------------------- If you are into Golang, you should also check `REST Layer`_. Developed by @@ -152,7 +137,6 @@ Olivier Poitrey, a long time Eve contributor and sustainer. REST Layer is .. _`Auto generate API docs`: http://blog.python-eve.org/eve-docs .. _charlesflynn/eve-docs: https://github.com/charlesflynn/eve-docs .. _eve-mocker: https://github.com/tsileo/eve-mocker -.. _Eve-docs: https://github.com/charlesflynn/eve-docs .. _`get in touch`: mailto:eve@nicolaiarocci.com .. _Eve-Mongoengine: https://github.com/hellerstanislav/eve-mongoengine .. _Eve-Elastic: https://github.com/petrjasek/eve-elastic From eb8c7afd519e03de407b4ba269ccf64d82a44fc5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 09:38:57 +0200 Subject: [PATCH 151/821] Changelog for #1009 --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index f405a4cf7..bf1f0bc36 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Development - Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose the whole document as found in the database, with no explicit validation schema. Addresses #995. +- Docs: add Eve-Healthcheck to extensions list (Luis Fernando Gomes). Stable ------ From f18c7c7f1a940a02ab35936263306aa6d3e63aa8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 09:39:20 +0200 Subject: [PATCH 152/821] Luis Fernando Gomes --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c33f0f160..5bfbfbb2f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -84,6 +84,7 @@ Patches and Contributions - Kurt Bonne - Kurt Doherty - Luca Di Gaspero +- Luis Fernando Gomes - Magdas Adrian - Mandar Vaze - Manquer From dd19fe9b76225b98e34f675fb2bff0abca5a4975 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 09:48:38 +0200 Subject: [PATCH 153/821] Docs: show GitHub stars instead of Watchers --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index a48fbbca8..7644493a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -160,6 +160,7 @@ 'logo': 'eve-sidebar.png', 'github_user': 'pyeve', 'github_repo': 'eve', + 'github_type': 'star', 'github_banner': 'forkme_right_green_007200.png', 'show_powered_by': False, } From 1e4343cdd3008c93e17e4b58fa3f96ecfdf28ecf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 17:52:37 +0200 Subject: [PATCH 154/821] Bump version to 0.7.3 --- CHANGES | 2 ++ eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index bf1f0bc36..6d04572e6 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,8 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Version 0.7.3 +~~~~~~~~~~~~~ - Dev: use official Alabaster theme instead of custom fork. - Fix: docstrings typos (Martin Fous). - Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose diff --git a/eve/__init__.py b/eve/__init__.py index b938c8fcf..767f3cfeb 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.2' +__version__ = '0.7.3' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 729849486..18d803da6 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.2', + version='0.7.3', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 1c52dbddd328a777bbd05af16addc1ca8e83d6a6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 9 Apr 2017 09:33:34 +0200 Subject: [PATCH 155/821] Add bades to index.rst --- docs/conf.py | 2 +- docs/index.rst | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 7644493a0..0fedb3e79 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -157,7 +157,7 @@ } html_theme_options = { - 'logo': 'eve-sidebar.png', + 'logo': 'eve_leaf.png', 'github_user': 'pyeve', 'github_repo': 'eve', 'github_type': 'star', diff --git a/docs/index.rst b/docs/index.rst index 6f544fd8d..f71d57773 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,36 @@ .. meta:: :description: Python REST API Framework to effortlessly build and deploy full featured, highly customizable RESTful Web Services. -Python REST API Framework -========================= +.. title:: Python REST API Framework: Eve, the Simple Way to REST. + +Eve. The Simple Way to REST +=========================== + +Version |version|. + +.. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square + :target: https://pypi.org/project/eve + +.. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square + :target: https://travis-ci.org/pyeve/eve + +.. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square + :target: https://pypi.org/project/eve + +.. image:: https://img.shields.io/badge/license-BSD-blue.svg?style=flat-square + :target: https://en.wikipedia.org/wiki/BSD_License + +----- + Eve is an :doc:`open source ` Python REST API framework designed for human beings. It allows to effortlessly build and deploy highly customizable, fully featured RESTful Web Services. -Eve is powered by Flask_, Cerberus_, Events_ and MongoDB_. Support for -SQL-Alchemy, Elasticsearch and Neo4js as alternate backends is provided by +Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data +stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and -PyPy. +The codebase is thoroughly tested under Python 2.6-3.6, and PyPy. Eve is Simple ------------- From 8c2cd5494ff5d08548eb05c21e3370aea76a9d5d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 9 Apr 2017 09:36:48 +0200 Subject: [PATCH 156/821] Add eve_leaft.png --- docs/_static/eve_leaf.png | Bin 0 -> 22086 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/_static/eve_leaf.png diff --git a/docs/_static/eve_leaf.png b/docs/_static/eve_leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..cd01dab322cd4aec2e535c4a4ea9792322aec244 GIT binary patch literal 22086 zcmZs?1z4NGvn~!P6fLgBo#IY$cX#(-#a)8CLve>9#Vxo)ad&s8xJxN;)89Gg-usv2 z$&)Yp?at1;yV;$0cD_huMX67SpAn&;pgze+i>pFGLF+(1dhj11CH}mx(~vJ{7gZ@y zsOm|=-;nDuYjtf`Z3TH=GY2~+6QF~sIg_WIBcv7-6u&1g!K*4R`QObUzXZsw zTwNV`nVCI2JeWM#nH-!gnOS*wc$it(nAzAEAvG9XyzE^~JQ?j>DE>>x|Hu(HcQJFe zc67CNuqXW|*TmGp%~gP${9i}^`}N;?+ByF3j_h6j+bW2C%$_EW%&bf-%>RFlT&*qs zk9z-)R{!q&SF`_E^|6Sw% zXv+UjB(IXQwK>Gre-;U{@-zQ`%Kp1PKl49x|1b0Z3)}ycLckS7g!J@310{&~g=s$hXmTAn4l(b5w3obQed7$^hoArz`+40P`&zfa%qRP7s!R}r8=WRCkaBZl5d zO}rG!okiXh;)>TV`nDJmi+kv5i)gXZb&f1e?1Yh=W2$G|B&VLgGyZr}7mv$FhCq?^ zL;!!29ZNw`wjazZ?F1FDj*c3^gSg{5891CGS1Epz@IAc7OUS zV{}?Tlr#^GG;yM2anjgkA~ADnRQ}D4KUxlJxoCu{GJhIVN%f9#2#A9$@o191vtUK} z;F`DgvK(f^N*YI{-~G5Q zN(}Zy;9=yFT`)^ zjAgt^#=pYk+put2Sjo7S)40_91TLJK>y%km5sfwut$zVNC>mVIjo~u3%stJbXbG;pI1(gnb3KSKV61(P?9X-di2KXf8fzX+ zm%@z*XRcB>0%@j-;TpjHWFN180SlRP>n)|O1Q%%+K zjWkVb1KryZ?K=V#iS{cm3UV3$7>R-KC8KxI3QaHdI5g#82x6ZbZb%&tW=B7(;Al)8 z#*=8l8?6S|p&>pF{1M$b_DA2mrsARddsjwa=sCwVnpDsv8X&!>Ibq%b)>zVjg~^)^ z&D8&9Lm)6ZH5kV|n5J39^zD{n7SDqKL~xD!7Q_-C&;Obu!I59w_V)iw>cQ!(P1=84s=cj9)tpes_>O@9jST^%9E#ouEL(6V z%hmL7C%FQ;Y+`31AC`=4=q1sc43r{)p-Eo;L*C#itOVN3jrV+octi6wv8Sx zUrRT0F7TvT(ph1??sGIpS9ZGaQAmI)Oaw56JjMSRocUK4b_uQ>w4A$f7>YF=s1G;Z zQ!65Ne%~kt$V3I^V~Ub~f{3;b7aU(vW|c?Pp`!XEZ?iF{|qhJ@({FE!&yD!^I^kk5v($W_g>&PZcQq zlz;;>ck&e%wBH}nLK?gH_m8PIklQT;j5R;=jrf(1loETV3Z8YiZIuz)(wXVe}{t^&_Gk;Tn z{N2`&!^K^)Eu9s;6+AbZSX@)8qszXKnE73mM=U^^%W`_HWH+`e)Rws7cqUXsuHvwq zP9%dU5=vsn8Cd_>Ono7-9F^tp>n7gsxa8GUqI1FfMvWSxD9=4<%#qR^wEWyNNBEgn zXc%X728(=WP_8l*KBy_HS{HNV1hJ$(l&pb7!LQ8L6p^$Lg9XllCeh8f@UsO*tCMwL zDqz@3WJ1I|0!TEUE}4aa6*Ql;@X^@~vLiFw(90H|#X%|ftaQREERw9vTrC*UWa(GE zR$wkeJ1m2o_f7_VAGpI9Aj|DNa3Np9iwCgJTyemY55=ad>>`d&t|P@!=p~sDE~2Te z29;5{U2kbZt^;b)n{0?fU_BYJ2o!{jG=M(~2ZDoj-+}Bo`Ub_5JI9Xxg2SUXWSBPe zlsn%zu8WE5JD3AUm@mv7H8(%}APNwSlp)#Z@VN!8N+`>;lL`yyAT`hb)tH<5Gd}9V z+?*f90ZcF`gb}CSOv-_$v%`hVXqz9Ym%DIoHUe_}#e^Qu zscTxA!O?Z=Br|-=5R&O1i{;MxXd)|A5vNwT)9n%m!zP|bGPs$&Y7|Wp7(*%xlf!Lq z%{|70k8o>gk3WmwPq`2>y=*M9Pu!wWadBSg#4B&&Ld?#pnxD36$fVFzy_Q?Q$~*>j zdVh^{nU!>?!`=GuxMM%(zoE1Xp8kTM8c!P`kVxrJnGuOZ_r?S(`n9X0vJ0Z{ho0YeiwO8Sd26MF^dW!_Go= zhMxV_jV|#@w${%!7xVMK3+!|=N$uUG%P$bGC41=9^mu}8C0($DFAHrZCY4VXsmp`wenG$VqUoz8wk zWNI;6l}Ow0*laHx`e?{NC!)O?Y}?e{#ZcNC@0qHvERmtL*Kp?R7Ri{(-TtHcx*4k%v3 zl*_Ox1z4|;``|8!Re9b+@6{GW@`%u7Id(dRG;7Yn)@dy<$25FI!h0uTEUq3`JuihN zQ-=Wvt6K@;WUqc(4|aKo!y|e!7x{5S%P9%s5AH~x$7%#_Zg$Zkg*SK=Q^k%@uW>Kn z-U+DqX|&Qjhv6^b^-Xq`1mlNgs>5CV^-ovkA1XR2{hWk0odd+(WWD`4_5SoXm_ibF z9hdehX3;c!PCC_W+OJj#2`e2+el00>ti1=akFx7q5!}Vf&0KxDMEcA&9iTTz4gisA z31#Bn7i49kPjl?$=o1)~-PM5co3XJxyF)c(Lq{GL4EgL@0@kL6%!lH%&RRL4GC250 zZaVTR40U~>Flm;Mu{ienLzlsm>3JL<7>nJqPe^_r`PGjl zHZ*P&D*Bd}_G?8i$oim*EW$lz|Dr3YJkXYb@;^s?+894ogL{5^^_7?9dExADi;_{J zMySftB?w<P^0GGY$~e~m^yPWYRwEoI=QG~)b%_X z4;Q<*7BHtk>N>b@Z@Db94_~W_i*T)2HWtyP?g5&^5G?(D!JB*0{QK+plJQ=TSETlx zvn`iDSqU0QIDcFYS;XR2D41Jq9xy`#3m(AV1DQ>B0?06#*8LavG>bDCGm9$t#!Xe+ zE|*3nkub(_mkmf)r)Y}syrU#=IiAgtf8j!CNTy|sRpK{lKvah9&T57dcBUF85pPwwqRI2qN4#Ii$F>~Kfqd60FQXL$?lf&qPJ}^MX`g;MXpWE#aNL>3!=o5? z+^w8sl?yph%DmT42cp?Y6$({GV$@W%lTrsZr_AP z0{)jv^~8htaLqiOPw|=v`ju}Xb)A@;$r|dwwfJTU7N(c-g7IV-B)~Nk+s_vf8x`k(+`DC^zo+xF6w-NWDc{{>?d|!K<2VPCOYXG;t$?=LbUSvK=2yS>2L=PR}x1 zg{)1FrUM00kutx%4QCSqbFENPG+>2}YmgOwV)2pYM*(#rUJ)ff!GO+1q|eST34tIX zaa%isr)_v3|Naj()HjNSg)(~pgB(K~y2SPK>ZuK9RS!Y6eaw4ec&{U=IVcY>42(9O z9IYT+&!n0eQTLEUqPCB{H)Xj=IpG;yTis4=Z&NUQ>7U7!E~D2j~24ii!6Ws zqRBa8|2nhi!D{mw?kaozTDs}?0(Gi7ttLl9k$bW4q+`G5c%h%^1Kis#ei=+SY8HrK`!k^(Ee`&+N*dh(8AY9l`~nN|{7B%=AL5L^7;yKv8L_hdOekIituK;Q|dBA49mos9HzAo?Hw^qxp(X zF3G4IZ7vtT&~2+v&|&CXnlRObxG?63sEDh@0Vsub)X2-je|b%a_2)c-5Yy7$w1$z> z%aezc)E_G|v5#GtNGFacX&nT$bhh773bem|{yD-d5TH&S3>c>E8^LI8NMt~&zoK}O zI9K^5LGssnE7a&b{HjJ{mtypQccU+0ZFvP>hEHYKpc7dLW>LAmh}wo^TuZYHOwiVZ zU(Kw6CKUh}?*Cr<)NGNsu~=L6K@TYOb5QW&qLGAt;3=xk{_8%f$Nqi{%Jqb`!}s1T9#(LZUo0F@6QI+%n`9TpLBC8MLo{Y8LDga zb#;CejOSMt2uEXlgf^Z0H$&kpCmJEbIy5`LK-Z(siuGbnXx_`9%c8j8ZCBLbuYz!D zwhdOJ>9+cTVq37Jt4C<9r+2+0qtz^nU7vW|42`QQFZd@Vpn(jVPdZ7JOP2374n^Tb$~EmRyn)0~xI1N5VmK2%ADcZVv>TI~XtN<)$2(V4;w(Xo-q)3c$cMMhrvCr82%18q)fQ++O@ zxQF0iUkli`ilhoLV;Xq-o&R_M3#9=PC>+nlA6Z^+RH5)+Yv(l*nJCvf%Dqq+RRltT`|lzD`{sbZwD*RmHfzFMiVJ$|OIN-_&g`&(FWetP$WTjC z!WXDvqCv<3bwFh`;)XO%5^)wQ*NcbSg4zWT24ioW&Hz7+p)cU{R!)>kD0tmu$R0ul zhNaeK@TTDrB_NuI3TLD!a3`K?X&b+;&7~K2V4m(cdA)d}esVZ+6nBSjNS|&a1ch#J z?n|D)d`?q{js8-GcL-r-KS`@9F$`pIF7&%48(&g16k2D=rky+i@}iDJ^h9{KFBS{4 zNz^i)7$7C;fz;!GRBO5iJw9uWEL+?+& zC1hFowjO6=NNnO4C0rVoFg09$Nx7<4;sLg_ym$UPy@p-#Pk87Hjp+q+Q_a&66k6tb zK9L3^ycR{?6|gf>JSTtcj>W898wUb(hZlslCDl%<$6ipjdp#dMD(_sfYsfx^No|up zvehnH^%@tOKR6!jjz)i^q1H#hVxWeO)z5)YQhl;K<(do1G4muoEx@nOO3&`6K1VP} zB=#^N&!2+)@{%N_KC?-Gh#48d5*`HhBvr77{9E0~>Acxn+1fP2q2f7ht|_2Osx5ci zu10_YVyafyvrm*dmNx=zT-eaW!qgR{ia)>E7@OZNjkj!ftWD)O7-N5tAP@zYA{bJh zbtQAE7R;g1_AYNBk*;E5e%ce+$V%g00#R&YCTs>My)kUa!TkDsq&C%dU3tBzOvSA` zZoqmTvWu~GS(NVTFX1Z@nW2j(0cOC7cqI&T&9X*bNMVPO47M^V*H&j`)d?P4h+z-6%_jC{FZ%N^?Vsb6>eHG9V1ryjowQ_9+ zsChzKr}#>4Xge9TbjGa7TxQ4uLl%sIOrcXV?F0LxT~x; z6`$ygU0OG^Hs{gZFG)n^b1Z`l+U&P{#l>A?81-K931_9HSibO&A^k6s*$~0{CV$a0 zc;8H5q$$-n0du24=H1m56$HmzMM=}($^!*t3jHWMhnDbPe_Wqw1wb&ABBNPk!Os;X zd3w5sMpTrn`n%Ln#~V{+I;yvgXWu6n$YestGaA=(T6}leX_v~28B*$)0eFb%&9@;r z{ORolxqvYAHw17_^k2)u{@aJf^|s7Hg>O|AIewW2oBp<2ZJ}gSxmgalnF+}9 znp*4HPKkbvs42j&Bh`A%OkqYbFX5UBU%Uc(QqqmJ%LWzE&gmg!XsQopMLqc?Iuo4C z%?^0tbM;Ybs_vT9!?@}^xo`FPJ$)5qg&7B!p?U)l!(W7O7%0Il{H)lAG112F4;~D` zOg!fBM{DW&=Cpv(cvxWBju4_3eZ7HyY-g*YmwQtpZmbFxav9at_)e05cwDC+J#i8rMstI0X3xAW>?c$bso1M1b_AK-5CywDU=`260p zCe#cXvbjK_=4phtJ=s4QA<)gz_4r+NYAm~?=N-f!(oNw}4a0wPfyMW^5LcsQ;o>mt zP-xqQdX%~UNJ#f!AH*XsuH-@f@peh^UDrC}kzUsJ)?4*~al_NbTYkdNUG`+xOP*pS z6`PSifgSdf$|~FBSh#O^=+KTBA3M?<5SfgB;mkph2rP)&Tyw57JUUgt#G)CF^|E)dl1?u&*?*QUKU${gsgIi@bb-Z;d*UCtuYD#z zJ5xl%3`Ci$3<=L9USMor$6}pFs^P9uI2zM>_1g~AVPu6H2(~xxSv!$!b>?Qn zO+q-Ar*E}kezf8{^W7z$v+(3u9K)`z{Dk$G)Ynrzk;7&2)1K?NFqwE~$ekDq$1Wlj zljPM>HdMnApL-|LcX~P@=7kURUEXlll@lB5z6&dOSX~7xle!X74_8Z}HY+J{m zmAJMx|Dy7(vb@LPr^Vx8d+Prd*Pb9V(#R+|_+@|mih-7(TPH54cQ;IPx?)uNNu z5DZk||DX}T!+UEbAJJtMlH{|uI_Tan`P9X{%ZDT04xm+KSnG?&@LT?N{-i#kC-AaI zW&S2rPo{0(YG@Ej;WDB8r?bx?0_kd++Ie+;+5tjfpAtXeSN^=CkKa4O{XPnjd0{d9U zCSPs+pIto}ERQaac;yzea zY#zAcEt*M4DH(XC^hfgf1bd$#k;;;Cj8gjXD~VBAbc1CYlqHm7g}Zyb zT~ta&OY=<{T?axrn0C**H>&l>QZ)}6F$RDo<{66fkq9L$NQc>h3sBJpvWZ5F_SL)Az2p)VOe>p?rLX$~?|~#Quk4)b zl6WA?#c1QrbP|_ZC}kseWJM*cMjQ)oY&PguP{qtVO)KKd`rVfxl=2?-TuU@rT&~Tz zHT@mizo%+UFNR@+A?B%0K-*{9FF*c#Wuwo%$sN48S-Sdht)vNR$^jwqAj`fdXV(z4&xGn zX{EI1MjZ4tQt}suAS;?#Oz5*0%n^EI=rvJn=3(F?fLdAu!~^VfCPzo^oU9WVY>HgK zGz!4H@=Ex97p~8|oXsp{JOta>8=p2w>-)`N&rl2(qk*gW!FY^P@v!{8)OHcgo`%-=(Pb-H3DD=UmQt8;U8oBu0Jrqv$ z6@;#(kP3Uew?2Sz`^F z?F}lubZ#fE__u$ce-X3_qan?+5B$pnaj(u*3L|lt3Kj(t(S2+wyyiV#GlajxJV&tmo3c5Lps#}F<*+#Ny`4Wd zKOFS1e+K_7dQ|A#=VN-$f;<&?3qc}q$7W(Emfq79n8EyvV{r5ZaQx~+M27KvW7`+u zJ_lAHMqD0eKGK^mhPkp}Kx-QEfQ#ktzHHp`2FysoI~1toDS!$Ed=K)<9){G9CJgr)bcN8}CiJhMJF-qRAO#X!P;P22})~2kOv5_R`HDO)n!9AvD zB##D#^L97f0>~>hpQ(g~oDCA?{&lpwz<;*0PWaop$x439p2O?;Tx4r1pq3_u?4MNq5wnzc5ar^^lyi-C1>;hpa4Xm|z;(`P+rqA`NF-G@+zK z(h&}16ato~_ebGY`l?UQx8ciR;1VP_@!YS^7c1oFWjv}J5n{hG6yY>x%e1xXy# zh`zf~07BpY=IzEIO0oeQJ;Q_?@5jz7(;KNtbgEnIq`5Smw~-3? zZnx>xwIpS;H=TKQQJZtGp-7sQO4 zNOg>N|Ly?dhhDv?Vwu}u+>ygjWTQCGy?z!Uj+!yU5SK$dXr4}uRIwYx;q@QQ>-u9K zrZTyJjt{f-QoWnmOhL*~++q0~&%@5oO}|IHeaVf^i;uTo9_6$3N3CXp$Xl{=W%@l= z3m2lxaZjvi^Q=FTU7LKcBw}Na1qzJ!;gT)~1*2(+B*ioMf3-E)mY}yJDlzYN({0$U z?0#pc>a0WOVrR$#pe7)KHVbJ5z^1n_RK-K+EeBM?#S=Srnu6;!z66 z6c&kYgEB_|cFsN{fY_Gsi86INw}c)*-acqe5Gb4IT3wA-jOMQUYHb(;wMMPUtpAuT zaD8xn*R5G(MQ1K!PVs7cL#f-4kIX1_bhC~9-kgp+cb>BRLvP_YOsTD;j6+*D8Z_|x zLXVLMgvJYM={fQs%40m1pbE!fY++@Ulrp413%KtN2c`MdAF~c>Ww`OeYUr>C#z7B` zobPH(Z&ex|y4fqa_nzP*trM%@9%*%1?JS#(L7-Aw7H%hk?kE@3Yi&?XJFm^s;=By( zAhFUKa;8$M)>i+%NMnF%cP(i`pXlagFiE8|LvQ#)s&LSp^Z}YtHzN4^))p0>B?wwX z1q)OK463kAe*e;}cRZ0ir1+>GRIy0wTX*(smf#zsa`P#norFL^vT?NWZ-3g-Y4yxE z{Wj+YD>5QBuQWsQ=$o)U|GUgsqgwYvMl&fKTaVU$z=SA+=JfD%_!ZB>!dM^7F~_56 zt9>fJ&g-Zb`c~tfC*|1<%Bf>H-31Hr1K+W*LGgPySC;arj3&{B zvV>}_MK{`=Ap-~M>*}m1GhLH$Mr%6+dqbIN^Uf~}ox@w_7|bz}wTl38{JxK;fBCNY z2W!3)pLhEo=TcsGKwNw8lk+R#RqY{!C$n!bGmAHNakDF|{tI{Ye)-{$%gc!&FqDUKO6JIKu}FYvkbYQ|DR_HcI3=XaHR_>57j!`V zH5q7gZ%cf3a1h>+;XTSBky`01!?-jB*`UxOiMz>{G69qtqVWCbXv+wYFoXZ(#F2b? zJ802`VhF2a8?{AluUXCamQ@Fi7?aMv@~aGUJNb`cj$v>PoMclU>>{;JgS3m-t&vEB z(#gU!WjLC05R{>f(8)dcyx9DWN0!>C&qO9td+orfeSOYNP1p2+fG*&gY2{vo3r{;2 z*y^OBlpLOnWNt>ALIpEVKD~x}f3`MPSgK9CJDkf9@2ge;AF#vTTh=3~(u{Ey8Ch3J z-DN8R*7I+;H+VX)lBvy7M1WR9Tdr@8zjqB~Upx?gtP%%J4QQA1v5JiGbZ`j zYEI4)ob&EO(rswCaa64tTF~h zg?)8qzyX{kHRZ}7mcUizR<4}{cz@PB%X02})WsSJaDzzf$cX5qi*q_`Lpv_lr)9lB zTm6I&beG<9oA-YGe2%m<>5~sRO)G20j0YnnjjbK>%8}^UmvlAYt066PhJj4{xzm-?kZ`=zL-7^qA~E++cm3?eE}sz ze0%X;&;)e-zOMV)K!{1E&2fm<#6>F-4l7`6_l8zAp+m^qnovwIg6wA-3MK99%Ri|QYkCau#0$GqktsOP6@n_1_-V0Nkv$~swN7B!Tfmn8)&s2cTg z^-ys8MJUs%zhd0wAi;dSJ`+kOp&YwDC$>1y>-hA9*xHn1a4o0jFISMYKAK%B@ncG`C4hAT*IF&br-465)?17wWNys?-%ZlTd zXM&5+7YYKJy_qLR)1SeR-p^tJ#&gf$U41TM|8R#f8L{_NchKl?rUxZ#2nYxi`W86o z#|#WoOr#lX8jEJ~z;z-+>|mF&6bbdN(=lUwA7bNEt%R`uCuwprVR%YX zN3GkNLmMZ%ZwavEDoM^8oQ zpUApl0^G%*{KcBN7#-TEaJ?}{4gvQJr~FlK91b!?z!>_&&+$K^>R$V>!r?uUC*wtp zkpzLgG>>1&Zgasdozmd`#3()z=)Vf<%iv0LjcNv}#0l)1!v%VE2;u!GhGS+bTG_3I z3?Qaqb0Va%Dn);6h)80ytG72?Q&t!x4|v+S;YK??5HYJ5VaSo@Vi)xCX!ddPvKB^; zi3T6z#n6zzie7m52M@v0?BG8U=6U2N^ty|a0)jdLFfU{J^h%Ujr#Q7buh`rg**mH0 zNUW0lsO5R~?DsMoe}zfm9rC9Bg%$=84>3k`e_(T*u=wA{$foJFSikpnB?XcFRw|&Z zUY)PC4wrN)kJk7XYo?7tnqJF^jtoUy0F*+J>pqi3Rvf&zNyxX? z33`hyp`> zOPXI>y5rB^YH8*c$EJ2tta%JGxKQ*0C-GTkxHKPzaeE1zpy54u`$2BiLH+1}5%@ES zBh$8(oM<4GssZN6PtgH#YoyC`p0RVcja_C(tu_Hu|9%eQ?{C2d29V&5*f(6 z-Q)3@#08;_&5D&{ zw{jdXwSp95*BIX+WG!dUxm_Lh9L0p>WpRO!uP3h<2COcdCH0piD`na*E7zve?R06HKN&T znd^!-k-&pdpLPKw^PazCH4fgw>36E~>q7jr2KW|2b$ z87$0CH^86h-Nc)H)s6&)i_uQJI&5$bHA^MeTzmG{y!^GV{3V;@lnzuZ_i+*dh3^i) z_DOSGa1Hjk@3D5z;ob-&ROoDc?Y$Ymx4ZOUA=lYkT|-Q~9^VL(_}Hu~f?LH4$-Ni9 zn0>J@3dpDBetEs9-*Hb_sGuVuUXI)e%-0BmOE#N{41X7CJ~phseuyQZWllAi{1>=o zWctL9Z}cijnMIVF%FI~nz|CyaHFo2TQb_v!j0Ng?O^e@gN57lJWb9jt3NffqPjN|dhBLz5p3_gGmI!5>}Vt3L}qc! zDdnyGU}0X^^{n_}5+RB3P;ECqqN9UtfXMhaU+N9dI0ai9bjQ2X?;I1NN+@D6|$av46v-+6Yr- z{UAE?s)&CPH%qh$7FbZK;{49w`205*H|NXPcM@{<9^^DV4q!aJAb$@oZvUA8f5M+H zvN?a0F5V81o^k`EL^<42$IFlw$6LS+3|DG1IjckIkgddzt9=d5m$^Z;)~@qvOfT{E zSK;m8p!XkpcaVM>$hUgN8#x~HbEtz{=fb^lYp7bBw%6eWah(KOJ8YF?uoh~omGpRj z2l_y1-7jWsb42vaFyTBAxNyPN0YYv6|FcMGKcvVmKcMMX!HF!G;chE zTIR;XS8z)GE@oX9SjsrJ61NkGdh^Xw7+suHmw0|}h-syfYrzjFi;$`%Nm&8!OmCUU z@)3kTXq;CIPFy)T)J}XIEyN{STEl(K#jU8_*5aXka%;*rr<)3g6a1T>MP%iX@F+|j z+_)7@de>9K!r27y2Rb;i-d?7Ay+)|KX{0-I93qRVSeQ2u!BUT*04?NmWmnr1kf zYti|QT^q%t9Z{n2{RktpqON1%F1?6@Kfg85-$?fibonb!?3(?#x7vPjLu~9D zXJPJG5TJebi(hSDZ|PGYv7&PIg-dhAIi_I-lG4OLde5KaO?xfb^OKM*`uD;qic9F? zX=OoCV-^70izI~Zz6N9FfXww0J9jPfp0znJItNbCnR@8*g{w15{XLB#vlM0aO1OG8 z;G_{9fTBZJ@zF_*I&hvg`O9S*aV|UcXY{TQ-6PCo+1p>6`6{NNGs;z6<55x1`gel_ z8QAjwId5zfP+DEZ+C`=>+AtYagx*Uxlw%v!t4N_^Fp&nAH!VtHA)0b`UDlq)$9>{o zQiYA#8$8kcO3c$+nfs08ps8_@b{15^6zGwzCd?eJP*F#89G|j{(ss@EuQyV|QV%3G zjWt$MNh{U2^`ECD)W}uOEaHbaiR`t?gN#ZNOSCd$Hd-FfE=*snJ%yR2W#T5`1B`2e z&E9nzP5))nB8tL>qVuR8r*0Ip;?QH6*C{*8gpwYs-iafTwL|S2gxson!V!`?4MquX zhF7Dad!R>jHso#N--^b6tlZZv&?G6@Z^jz&d%nJnN4T$*uy6L{S+C}R?dAfTki*L? zhfRk>3_1WgDYMX~KLj!dE)&m)DRZ$w@+CHO(=;sAYEniIvw^K^8tSRl;tby%dil0t z=mV2V<#0RT+VW=Db`a*bzoaX6cioIU# z%om*UBl7vGC!FLL2x_el;(0G4Ltlvd`!ydH4R_LDLKR5zxwnJLtmT zH+>Z_KoBucfCJqXgDJ{ztCC6d3k#BlXn-X z-U31OeSi!>50iqVHytD|D!NXIyT*32nt|_N7Ade&#(i^C#|*Z{wQ=dZZTQqJFKn~i zi1}bVwJ#jbot(pjnURTwz`J}383x$2ap0`L>2fQ_J`VOKDwn}w!hb`j%BAlxyvB%M za{D=T#I)d%l)5e~+4rHfH|HBCQQ*?ft@q^5gDjLxLzi65(g@&DO|BQKrtYgy{E|U% zRqc>Pl%|6e;UnooPuJ_dfbmchMGhw^X>Apm(eb+7{hBvt@=tO)bZHr|i`E342WVx9 z-`=Th>QWGAue{Bkwp3NB4=hDK`(r0iP*>j=ZQ;G|)K>GNE5Zx(OMD$a(MjUBfBsh4&<2%-kQ*cc?eitOF#ckaWv#IyT4Ds$Gjo1s zy!QrpUvjBPRo@6ds4`kIrQt{MYFkp(21TAD~T0C=ty_hv;>*E=4Nth)>Fm#AzU%rk9lWVgv|0ldj!EQ<+ z8$ui+9@M^7^ls{wxW^u6&ack@iCG0F8}R^M1y)X|7Lb%fF6)z=w0q7m@CMB?2-bp) zPi@0sVV|xp#g6QK9=DDjQMQ^fc%-iTdbzp!n>a?%3$jB3V9|w{@3O8yosYHOL^TC; zscS^we_d)%%S)AVuu$vO7ag8z6BU3OoYDDsUD$|8tr9ND3J%4`=brC>-1>n90ybL`JJv}9x-(sN$fnbkL7L*xr8--E_OL>-2~s7zWGup9ePZv1`=>vG zLR0sw!lU7!k?@E=Xk_goliBI4VcFAJgGg$bpPn8}t&A_)s^oaF~v(=Nl@lQ&1}<8mKC688im6gYuPRQ%waTHjyXPcW3o}O#Lxjt}kJSA|;xh z`7;yAhE`k;TH-w{g{c2zSMJ5QK8OW8z!5y2>1?D&PpYQNu5gKqy7;@Qk#B}JK@dC*#D{zw_S`y^d{@q*)W*&VjjGTAl{yucWN zm9Mfsr-*+Gt~E@2rbv0=p+!kO;At-gsLO{x^t%4x!~F#k_KnvxrjZ8UnL-ZWXT>vI ztC684D90o?IXUclY(}+GB8&AQ&rBPm0SX&OC&H{mCM};}-$_Oxn?b{3Ud@5}IYx8< z)_e9>H&awYo~N&jME`0F0?cT%l-@oBwVlnX-fs?e*3IGx|5lt_g8DE>n!Pkz0D=?O9+0SoAO zGdk1H8MZ0=Yf}MM%=4lRr7z`>2aCt*7syCPZK!kx{Sn#=H*T->9-4iiqHHN)DnkM& zrL~TKC~1n**GiDCN_Zdd0P%G^9!r>Vc&Vv}GNEQ=6hB7UQot~T1%zN{Bs~X_3Eb@( ze&`b1x}34*)e7#YA){N4=S&`pCeH+com^Xjo#%;iKK-u%Srn%07mwZXH2Nk42m!+p z5LuLcL9bc{dbC*RsX^xb2>Sn7J^Fe#000ynNklWt?#;I7mst$ znHQ*$k%WK+5P;wM6?&@CPqcDq4tld&Rmv67$@)dj;+i=s%jHFOT}YNAwW(p#Ke2BC zAB+#@1#)C0A&`3nPOe&9RjsEQJTO#93eD0$v>wE;eF{tPYxLB|39@t}Sq?`3oG!6(M`?rr zA)qS)ogshdDE_Lit5gW%u?l>m?*+Jcovl*#J`5jJ!JqzZmE|0nUDuv$93jj!-iKF* zAI7ign-Cxb3`3v=Gr$N42qCEvZ^c%2^O|enB*+H(MB+H|+%VGI)0I|sqV33Z4 zfGrWgySWe*{-2S$XF;^(*v95b9yHuY{j4n8P?F__x2$RD!={WE;#BRgpp|hW-3S3o zA%Hm$PO^qfWXBE7f@o)z21_3@TEDoSR{J)bWVyk7Tqh>QxTEq8s{ugz5CRTC;CO7~ z{b{3Zi4!fn2mYiwD zhDw}hB}tl%`IhZ!>O*h)2JC)&t(r1U$ZP29MLK{gmsHLG;qqU#Po~R5>4X3wpbi1_ zE_*~tJgR9$Q8A8tqvkCnVbs zRXvlM8=4>U`n{*%HR47zRJ%PPB&`Vn>mUH4y~0dwG!m@`I{vQurs_|ascWw4%E@p+ zDq}HdKSE;C&A9h1eDc=aBNaB3MhK`u0K*0+nZ2Y{3HXg-dft(=sFE(rlbKmh_~_hz|Dyhn>SQXcM9O%U;;FgNqO(^rLA zrYD>H>{Q-Zd8X(RS0X)c#%_`^pld=vBLc0M^Ej4aNKRvU&BdygRGkQtZ-jseDuirC zkelg71i4Y6gJD@MOR{l`c_3i?LN?&~WAQZlCIkq9%m@gQFbiG=f6AQ8n7fCGwgu8h ztEk~QEW^K;uM$;J#uiyEGhC1kt)_;i4fs|51l&ZfL>@J-BI!g@Iw9bA1hAC#OSv+V zj|YR~`QoE)6*`=cZN~XNVV3Xssj#U(t4nQxoaAq0a_gqne@r}U;{Bpq?12uS!pENz zU8qt*z#a&kG`42!Js&iE5XyzRtxc`FCQX^N4Rv3Nk5ZobmhX2^A;&H0FwNl06e2TiKw>V9HDgXh*>95j^kX@8xgWMPyeJb#I?I zW#Zcyqg|jv(x)fYOxgpI&Y9jSbXAnqmIU=)%X__Oz&}o^n)FL-i##5Uc$y+>DH9=( zM+m?{XH46sww%UvM*Xi(oI0@sf1}* zL9xvZo1d+%uiX$1g})@r@(=NfGG2oiDw10SI#B&uY+dr89FsjUA?6iCp$O*%2u9+? z4{0fSXI`{zA=ajsdO0NDk?y9+=`xnke>EHok43%bE2#UY{X45D$Y}K}juVNjqvq!mQHy>*~B}qpKZztEZ zG_>5Cly2)w$CGTlb*3zx@)7LLuuKpHL$*(j*F)cm5!jF3)>?1KdyiQ?*HoHeb<#Y% z?ff>?J-emh*}m7~^W4x_-x##I?&sWm&ZKB8%2$QOm)}P4L-#o>WLRf)-PP)sHzXSe zrgCXz4Q4HQ!0_sL8hx9Jz<%^rR`~+Hdq4(iRs`v;WJP^Ni7V*h6Q%J#blvMPN#K5r zRey=kNtjfhF4Z8^;pr^KHe!vJh;jG1O}95WY7s&lq`V{9xZyQRYv$vRox7Es6i=gX zLlLkHxyG%8TM>UP{YL!Srr&?Y64oyuPQfla>R+|Esv0EQhaT*!cGO>q&Us6+92|He zxI=xRui-U|kJ<8BPdfBRz;fiuP2_*v7$(?+KXAML4MfpN_+Cq(WkoKcZR!7*I9H-w zA23~81Y}zTx39%~Mizp(8xS4YBk&_*?Pfd$!vEo)l(l$J9y zAHrwe0ym)_m|3Tebv=gRfp3_#S)z`5=dCCr*%VD*xv26~QS6@_yD-IVN=ku%9mtid zU9+g>WP|~H4X1Y7;;0)v@2_K**T+-9Pe~R=fJin&<5VxMJ_o(BRmj3)BhZlo0lSdv zVbujpb~WPsoD`;5IVl>8#Xh=a<(9XstR(4{8UZ5N)VL(33V6M&!|UZTBp#o5BcIQ0 zMy~x$RlT(Os~C6ZfuZeo{26Qir`t_w()SPoPG7sNM?1?{J@Z6|zvEQ&YQGJE`}GiN zV;QY@ZN0jzr|eiPeOznRao1#g&${uf@gP{fXPo^GnLTYkrAgyB0*(q06F16;?>Fk- z2t)2&<%`RI?ic+_Fmrw_u6dvls~o#PB5&Y_z>DvJASzI<5TAH(lx%e6va%AB65TP4Hswx{_sBY+!Nsn5L@nW6xdd{^V}Ie z7+$yS$J-*A%;By}W7@|Q^m+S=KssJYluEEt$cJ_0fv6lU#F7%049fgq9m5^ zh*wD4fu8&$b|lyNtEL?jk=V-p2ZD8(u(9ktnf)DsUdk=2A1O900=XlHVO6t71_Yov z!!5ZRFh+Tvi5ka1eoukh*ETO}=Ig)dNJ8M^wbRG>T;k7*i~O^8?C21B`=UuXY;q~H z?}220WD{*k%@ha_$)*5BOH$QMRnvu-umXKrzVDb($3Y1GvD@Xo_pi78wbiIf>@4?! zWm85J78X2QSnR#nu;ZH;+x#vFw@j;O5bGZi zO!`g)&v+g(WUX?XT<)p|%RT#pqwX(qOJDSi@_7<%-M6>bz68q+5y-0`u|4g2PCZ!z zTW-pdDI-9VH=~Mg;^R|TRWHVOAH^ToomSWYPod7HXRF6s!@ow%yrZzt`*p9+H3Zuz zX1m^c`yk`JS-Tzx$vM>t8wuJKf4%f3lGO{z405V(u095FGjBs*ln*7O!@L#u?~%ju zPvLudAcMHPzcqjD%-f0zyw`aHF1~+P%41hsCu{HEUU#-V_BQ*Q$1}Vg+13J5ydQyd zfA9p40SHvySb1iDOay@?uHI?Eb|Gs$0ncv{#M-7);0Az|Lgd1=GZzO6+~4&DJQ??F z>+6lMci-i^+SvDiANE7ohUF{vwSY8_BS2%aaU@Lm-n?@2-%-sB#4?=?2lIJw!+MBs zXKq5(wyt9S9n-&2P~^MKA8;!g-~MoSkFD+H%HxISceXcz4CqIIdb0E&SQdc=t7m@R zU*ujBDDaF{s-*2*wqPj6g0QF?8+ctTt*@89G=XwL)`wI&_ z^#nbCP?C}I z*aHC~S$njTBek5nX67gSUe{xVMc&hdEQ4qcC>{R zJnW#`>v^oW#5+ZBWlyegqj!H`f0wPeEzC_zX$P=y$O)0GLmJY`X7I3sewXX9qGI24 z(Ix1|t}wa-QMUSmj#vKh-B4cMkN#?PK#1L zXYVLmNr+&{HIZx*CQi82_0`P@fRGK@BT%)Zst&W{Ph>ACuOz&MCDtFwD~t2Q*eo4q z@&qL^Q!xTHi)xBNuxk_-pp5+pU@0y2VjcP{5Xm~UVRK~`g5K1l?g_c7->&syl@|qs zv1{WEqc@T4FaXhQgoZN_Ad+=vyXL|wK@z0< zQE~tNTDefbu61H~a#*%4k~zh-7Wsbh%YL5{;~d21U8m!j`pn zLF*+Q{*Kc&FWcN?OAAP8RR~ak7*%jlW{yD3(wg&SS*Crc)I1JY)&)<3N29Uml2=x~ zvRh42%4=@~h-B^Eex_?*bzN0J@CmP@;^R%P+@=+RU$-!cUDL9nWvfk1AhokVfa1w! z0bV{91;O47yNO_xHU0;P<*>MAS<7R{ZvR~#mDx)&Ol_+0aUg{OR zZz3e$_`EZIID=)@j)|>$U%+>7V|}Azw#^;RY`U);0qV&r$0~1gf?$6{1UsbpB`n7H z2{u3c-o|Ac_YS#B353951gIxVk3vQSDsQYjQ*?>1AQLTIIfB$ipm9y}?alun6J98`fKp> zJSz`PPc}J-ga$bzH#Dzl?n}N&*MvYO1ZcP*6FPZ(ulk1SvDmHazaZJNJcfb6ZS4s` z6c#n#*8F}x<3P4+M}T^=+OczXNjF;!1M`_+{|SQqHdbr>#n}x_))|ce^<<4k*oN|_ zESXY^FnllDQ1xt8-U9*8fLzy4e0$=Cb?er}vK2_>2?6TK(lam!f%2=$Js{Zo1|>TC z;w|{OKFx`Bvu|??Ci^63n@iT2jR29X*@#=bKL1Gn4IrIUEv}|^^qUFx3sVH5`^u$5rlh&4K&{g9oO?u=s@V@Z9m=?v4PH{qQem&l680k>t!Bd$DF3ueaIHUT`%xQd z#g#@BshFh@Ad6Q3lDV*dXxxLz+Zas8%-O*^cmHEBZ#=!*c6tiE`Xv=j(n z-1cY;A$$cBSMOjc>mN2P-*hlVHcBD{9ErgH2MB)pPv;i8@c;k-07*qoM6N<$g8gZJ AsQ>@~ literal 0 HcmV?d00001 From a7350d08f4fc2e5155cf62e8f618401df14cb482 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Thu, 20 Apr 2017 19:32:19 +0200 Subject: [PATCH 157/821] Added check for resource in config.URLS --- eve/methods/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 6862f9031..9cbde3604 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1124,7 +1124,7 @@ def oplog_push(resource, document, op, id=None): .. versionadded:: 0.5 """ - if not config.OPLOG or op not in config.OPLOG_METHODS: + if not config.OPLOG or op not in config.OPLOG_METHODS or resource not in config.URLS: return resource_def = config.DOMAIN[resource] From 47e4f52fad6a5307af41d9b481fd543b13c255af Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Thu, 20 Apr 2017 20:56:10 +0200 Subject: [PATCH 158/821] Splitted long line to conform to flake8 --- eve/methods/common.py | 2004 +++++++++++++++++++++-------------------- 1 file changed, 1006 insertions(+), 998 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 9cbde3604..73704bc5d 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -34,1145 +34,1153 @@ def get_document(resource, concurrency_check, **lookup): - """ Retrieves and return a single document. Since this function is used by - the editing methods (PUT, PATCH, DELETE), we make sure that the client - request references the current representation of the document before - returning it. However, this concurrency control may be turned off by - internal functions. If resource enables soft delete, soft deleted documents - will be returned, and must be handled by callers. - - :param resource: the name of the resource to which the document belongs to. - :param concurrency_check: boolean check for concurrency control - :param **lookup: document lookup query - - .. versionchanged:: 0.6 - Return soft deleted documents. - - .. versionchanged:: 0.5 - Concurrency control optional for internal functions. - ETAG are now stored with the document (#369). - - .. versionchanged:: 0.0.9 - More informative error messages. - - .. versionchanged:: 0.0.5 - Pass current resource to ``parse_request``, allowing for proper - processing of new configuration settings: `filters`, `sorting`, `paging`. - """ - req = parse_request(resource) - if config.DOMAIN[resource]['soft_delete']: - # get_document should always fetch soft deleted documents from the db - # callers must handle soft deleted documents - req.show_deleted = True - - document = app.data.find_one(resource, req, **lookup) - if document: - e_if_m = config.ENFORCE_IF_MATCH - if_m = config.IF_MATCH - if not req.if_match and e_if_m and if_m and concurrency_check: - # we don't allow editing unless the client provides an etag - # for the document or explicitly decides to allow editing by either - # disabling the ``concurrency_check`` or ``IF_MATCH`` or - # ``ENFORCE_IF_MATCH`` fields. - abort(428, description='To edit a document ' - 'its etag must be provided using the If-Match header') - - # ensure the retrieved document has LAST_UPDATED and DATE_CREATED, - # eventually with same default values as in GET. - document[config.LAST_UPDATED] = last_updated(document) - document[config.DATE_CREATED] = date_created(document) - - if req.if_match and concurrency_check: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] - etag = document.get(config.ETAG, document_etag(document, - ignore_fields=ignore_fields)) - if req.if_match != etag: - # client and server etags must match, or we don't allow editing - # (ensures that client's version of the document is up to date) - abort(412, description='Client and server etags don\'t match') - - return document + """ Retrieves and return a single document. Since this function is used by + the editing methods (PUT, PATCH, DELETE), we make sure that the client + request references the current representation of the document before + returning it. However, this concurrency control may be turned off by + internal functions. If resource enables soft delete, soft deleted documents + will be returned, and must be handled by callers. + + :param resource: the name of the resource to which the document belongs to. + :param concurrency_check: boolean check for concurrency control + :param **lookup: document lookup query + + .. versionchanged:: 0.6 + Return soft deleted documents. + + .. versionchanged:: 0.5 + Concurrency control optional for internal functions. + ETAG are now stored with the document (#369). + + .. versionchanged:: 0.0.9 + More informative error messages. + + .. versionchanged:: 0.0.5 + Pass current resource to ``parse_request``, allowing for proper + processing of new configuration settings: `filters`, `sorting`, `paging`. + """ + req = parse_request(resource) + if config.DOMAIN[resource]['soft_delete']: + # get_document should always fetch soft deleted documents from the db + # callers must handle soft deleted documents + req.show_deleted = True + + document = app.data.find_one(resource, req, **lookup) + if document: + e_if_m = config.ENFORCE_IF_MATCH + if_m = config.IF_MATCH + if not req.if_match and e_if_m and if_m and concurrency_check: + # we don't allow editing unless the client provides an etag + # for the document or explicitly decides to allow editing by either + # disabling the ``concurrency_check`` or ``IF_MATCH`` or + # ``ENFORCE_IF_MATCH`` fields. + abort(428, description='To edit a document ' + 'its etag must be provided using the If-Match header') + + # ensure the retrieved document has LAST_UPDATED and DATE_CREATED, + # eventually with same default values as in GET. + document[config.LAST_UPDATED] = last_updated(document) + document[config.DATE_CREATED] = date_created(document) + + if req.if_match and concurrency_check: + ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] + etag = document.get(config.ETAG, document_etag(document, + ignore_fields=ignore_fields)) + if req.if_match != etag: + # client and server etags must match, or we don't allow editing + # (ensures that client's version of the document is up to date) + abort(412, description='Client and server etags don\'t match') + + return document def parse(value, resource): - """ Safely evaluates a string containing a Python expression. We are - receiving json and returning a dict. + """ Safely evaluates a string containing a Python expression. We are + receiving json and returning a dict. - :param value: the string to be evaluated. - :param resource: name of the involved resource. + :param value: the string to be evaluated. + :param resource: name of the involved resource. - .. versionchanged:: 0.1.1 - Serialize data-specific values as needed. + .. versionchanged:: 0.1.1 + Serialize data-specific values as needed. - .. versionchanged:: 0.1.0 - Support for PUT method. + .. versionchanged:: 0.1.0 + Support for PUT method. - .. versionchanged:: 0.0.5 - Support for 'application/json' Content-Type. + .. versionchanged:: 0.0.5 + Support for 'application/json' Content-Type. - .. versionchanged:: 0.0.4 - When parsing POST requests, eventual default values are injected in - parsed documents. - """ + .. versionchanged:: 0.0.4 + When parsing POST requests, eventual default values are injected in + parsed documents. + """ - try: - # assume it's not decoded to json yet (request Content-Type = form) - document = json.loads(value) - except: - # already a json - document = value + try: + # assume it's not decoded to json yet (request Content-Type = form) + document = json.loads(value) + except: + # already a json + document = value - # if needed, get field values serialized by the data diver being used. - # If any error occurs, assume validation will take care of it (i.e. a badly - # formatted objectid). - try: - document = serialize(document, resource) - except: - pass + # if needed, get field values serialized by the data diver being used. + # If any error occurs, assume validation will take care of it (i.e. a badly + # formatted objectid). + try: + document = serialize(document, resource) + except: + pass - return document + return document def payload(): - """ Performs sanity checks or decoding depending on the Content-Type, - then returns the request payload as a dict. If request Content-Type is - unsupported, aborts with a 400 (Bad Request). - - .. versionchanged:: 0.7 - Allow 'multipart/form-data' form fields to be JSON encoded, once the - MULTIPART_FORM_FIELDS_AS_JSON setting was been set. - - .. versionchanged:: 0.3 - Allow 'multipart/form-data' content type. - - .. versionchanged:: 0.1.1 - Payload returned as a standard python dict regardless of request content - type. - - .. versionchanged:: 0.0.9 - More informative error messages. - request.get_json() replaces the now deprecated request.json - - - .. versionchanged:: 0.0.7 - Native Flask request.json preferred over json.loads. - - .. versionadded: 0.0.5 - """ - content_type = request.headers.get('Content-Type', '').split(';')[0] - - if content_type == 'application/json': - return request.get_json() - elif content_type == 'application/x-www-form-urlencoded': - return multidict_to_dict(request.form) if len(request.form) else \ - abort(400, description='No form-urlencoded data supplied') - elif content_type == 'multipart/form-data': - # as multipart is also used for file uploads, we let an empty - # request.form go through as long as there are also files in the - # request. - if len(request.form) or len(request.files): - # merge form fields and request files, so we get a single payload - # to be validated against the resource schema. - - formItems = MultiDict(request.form) - - if config.MULTIPART_FORM_FIELDS_AS_JSON: - for key, lst in formItems.lists(): - new_lst = [] - for value in lst: - try: - new_lst.append(json.loads(value)) - except ValueError: - new_lst.append(json.loads('"{0}"'.format(value))) - formItems.setlist(key, new_lst) - - payload = CombinedMultiDict([formItems, request.files]) - return multidict_to_dict(payload) - - else: - abort(400, description='No multipart/form-data supplied') - else: - abort(400, description='Unknown or no Content-Type header supplied') + """ Performs sanity checks or decoding depending on the Content-Type, + then returns the request payload as a dict. If request Content-Type is + unsupported, aborts with a 400 (Bad Request). + + .. versionchanged:: 0.7 + Allow 'multipart/form-data' form fields to be JSON encoded, once the + MULTIPART_FORM_FIELDS_AS_JSON setting was been set. + + .. versionchanged:: 0.3 + Allow 'multipart/form-data' content type. + + .. versionchanged:: 0.1.1 + Payload returned as a standard python dict regardless of request content + type. + + .. versionchanged:: 0.0.9 + More informative error messages. + request.get_json() replaces the now deprecated request.json + + + .. versionchanged:: 0.0.7 + Native Flask request.json preferred over json.loads. + + .. versionadded: 0.0.5 + """ + content_type = request.headers.get('Content-Type', '').split(';')[0] + + if content_type == 'application/json': + return request.get_json() + elif content_type == 'application/x-www-form-urlencoded': + return multidict_to_dict(request.form) if len(request.form) else \ + abort(400, description='No form-urlencoded data supplied') + elif content_type == 'multipart/form-data': + # as multipart is also used for file uploads, we let an empty + # request.form go through as long as there are also files in the + # request. + if len(request.form) or len(request.files): + # merge form fields and request files, so we get a single payload + # to be validated against the resource schema. + + formItems = MultiDict(request.form) + + if config.MULTIPART_FORM_FIELDS_AS_JSON: + for key, lst in formItems.lists(): + new_lst = [] + for value in lst: + try: + new_lst.append(json.loads(value)) + except ValueError: + new_lst.append(json.loads('"{0}"'.format(value))) + formItems.setlist(key, new_lst) + + payload = CombinedMultiDict([formItems, request.files]) + return multidict_to_dict(payload) + + else: + abort(400, description='No multipart/form-data supplied') + else: + abort(400, description='Unknown or no Content-Type header supplied') def multidict_to_dict(multidict): - """ Convert a MultiDict containing form data into a regular dict. If the - config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the - same key get entered as a list. If it is False, the first entry is picked. - """ - if config.AUTO_COLLAPSE_MULTI_KEYS: - d = dict(multidict.lists()) - for key, value in d.items(): - if len(value) == 1: - d[key] = value[0] - return d - else: - return multidict.to_dict() + """ Convert a MultiDict containing form data into a regular dict. If the + config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the + same key get entered as a list. If it is False, the first entry is picked. + """ + if config.AUTO_COLLAPSE_MULTI_KEYS: + d = dict(multidict.lists()) + for key, value in d.items(): + if len(value) == 1: + d[key] = value[0] + return d + else: + return multidict.to_dict() class RateLimit(object): - """ Implements the Rate-Limiting logic using Redis as a backend. + """ Implements the Rate-Limiting logic using Redis as a backend. - :param key_prefix: the key used to uniquely identify a client. - :param limit: requests limit, per period. - :param period: limit validity period - :param send_x_headers: True if response headers are supposed to include - special 'X-RateLimit' headers + :param key_prefix: the key used to uniquely identify a client. + :param limit: requests limit, per period. + :param period: limit validity period + :param send_x_headers: True if response headers are supposed to include + special 'X-RateLimit' headers - .. versionadded:: 0.0.7 - """ - # Maybe has something complicated problems. + .. versionadded:: 0.0.7 + """ - def __init__(self, key, limit, period, send_x_headers=True): - self.reset = int(time.time()) + period - self.key = key - self.limit = limit - self.period = period - self.send_x_headers = send_x_headers - p = app.redis.pipeline() - p.incr(self.key) - p.expireat(self.key, self.reset) - self.current = p.execute()[0] + # Maybe has something complicated problems. - remaining = property(lambda x: x.limit - x.current) - over_limit = property(lambda x: x.current > x.limit) + def __init__(self, key, limit, period, send_x_headers=True): + self.reset = int(time.time()) + period + self.key = key + self.limit = limit + self.period = period + self.send_x_headers = send_x_headers + p = app.redis.pipeline() + p.incr(self.key) + p.expireat(self.key, self.reset) + self.current = p.execute()[0] + + remaining = property(lambda x: x.limit - x.current) + over_limit = property(lambda x: x.current > x.limit) def get_rate_limit(): - """ If available, returns a RateLimit instance which is valid for the - current request-response. + """ If available, returns a RateLimit instance which is valid for the + current request-response. - .. versionadded:: 0.0.7 - """ - return getattr(g, '_rate_limit', None) + .. versionadded:: 0.0.7 + """ + return getattr(g, '_rate_limit', None) def ratelimit(): - """ Enables support for Rate-Limits on API methods - The key is constructed by default from the remote address or the - authorization.username if authentication is being used. On - a authentication-only API, this will impose a ratelimit even on - non-authenticated users, reducing exposure to DDoS attacks. - - Before the function is executed it increments the rate limit with the help - of the RateLimit class and stores an instance on g as g._rate_limit. Also - if the client is indeed over limit, we return a 429, see - http://tools.ietf.org/html/draft-nottingham-http-new-status-04#section-4 - - .. versionadded:: 0.0.7 - """ - def decorator(f): - @wraps(f) - def rate_limited(*args, **kwargs): - method_limit = app.config.get('RATE_LIMIT_' + request.method) - if method_limit and app.redis: - limit = method_limit[0] - period = method_limit[1] - # If authorization is being used the key is 'username'. - # Else, fallback to client IP. - key = 'rate-limit/%s' % (request.authorization.username - if request.authorization else - request.remote_addr) - rlimit = RateLimit(key, limit, period, True) - if rlimit.over_limit: - return Response('Rate limit exceeded', 429) - # store the rate limit for further processing by - # send_response - g._rate_limit = rlimit - else: - g._rate_limit = None - return f(*args, **kwargs) - return rate_limited - return decorator + """ Enables support for Rate-Limits on API methods + The key is constructed by default from the remote address or the + authorization.username if authentication is being used. On + a authentication-only API, this will impose a ratelimit even on + non-authenticated users, reducing exposure to DDoS attacks. + + Before the function is executed it increments the rate limit with the help + of the RateLimit class and stores an instance on g as g._rate_limit. Also + if the client is indeed over limit, we return a 429, see + http://tools.ietf.org/html/draft-nottingham-http-new-status-04#section-4 + + .. versionadded:: 0.0.7 + """ + + def decorator(f): + @wraps(f) + def rate_limited(*args, **kwargs): + method_limit = app.config.get('RATE_LIMIT_' + request.method) + if method_limit and app.redis: + limit = method_limit[0] + period = method_limit[1] + # If authorization is being used the key is 'username'. + # Else, fallback to client IP. + key = 'rate-limit/%s' % (request.authorization.username + if request.authorization else + request.remote_addr) + rlimit = RateLimit(key, limit, period, True) + if rlimit.over_limit: + return Response('Rate limit exceeded', 429) + # store the rate limit for further processing by + # send_response + g._rate_limit = rlimit + else: + g._rate_limit = None + return f(*args, **kwargs) + + return rate_limited + + return decorator def last_updated(document): - """ Fixes document's LAST_UPDATED field value. Flask-PyMongo returns - timezone-aware values while stdlib datetime values are timezone-naive. - Comparisons between the two would fail. + """ Fixes document's LAST_UPDATED field value. Flask-PyMongo returns + timezone-aware values while stdlib datetime values are timezone-naive. + Comparisons between the two would fail. - If LAST_UPDATE is missing we assume that it has been created outside of the - API context and inject a default value, to allow for proper computing of - Last-Modified header tag. By design all documents return a LAST_UPDATED - (and we don't want to break existing clients). + If LAST_UPDATE is missing we assume that it has been created outside of the + API context and inject a default value, to allow for proper computing of + Last-Modified header tag. By design all documents return a LAST_UPDATED + (and we don't want to break existing clients). - :param document: the document to be processed. + :param document: the document to be processed. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - if config.LAST_UPDATED in document: - return document[config.LAST_UPDATED].replace(tzinfo=None) - else: - return epoch() + .. versionadded:: 0.0.5 + """ + if config.LAST_UPDATED in document: + return document[config.LAST_UPDATED].replace(tzinfo=None) + else: + return epoch() def date_created(document): - """ If DATE_CREATED is missing we assume that it has been created outside - of the API context and inject a default value. By design all documents - return a DATE_CREATED (and we dont' want to break existing clients). + """ If DATE_CREATED is missing we assume that it has been created outside + of the API context and inject a default value. By design all documents + return a DATE_CREATED (and we dont' want to break existing clients). - :param document: the document to be processed. + :param document: the document to be processed. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - return document[config.DATE_CREATED] if config.DATE_CREATED in document \ - else epoch() + .. versionadded:: 0.0.5 + """ + return document[config.DATE_CREATED] if config.DATE_CREATED in document \ + else epoch() def epoch(): - """ A datetime.min alternative which won't crash on us. + """ A datetime.min alternative which won't crash on us. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - return datetime(1970, 1, 1) + .. versionadded:: 0.0.5 + """ + return datetime(1970, 1, 1) def serialize(document, resource=None, schema=None, fields=None): - """ Recursively handles field values that require data-aware serialization. - Relies on the app.data.serializers dictionary. - - .. versionchanged:: 0.7 - Add support for normalizing anyof-like rules inside lists. See #876. - - .. versionchanged:: 0.6 - Add support for normalizing dotted fields. - - .. versionchanged:: 0.5.4 - Fix serialization of lists of lists. See # 614. - - .. versionchanged:: 0.5.3 - Don't block on custom serialization errors so the whole document can - be processed. See #568. - - .. versionchanged:: 0.5.2 - Fix serialization of keyschemas with objectids. See #525. - - .. versionchanged:: 0.3 - Fix serialization of sub-documents. See #244. - - .. versionadded:: 0.1.1 - """ - - normalize_dotted_fields(document) - - if app.data.serializers: - if resource: - schema = config.DOMAIN[resource]['schema'] - if not fields: - fields = document.keys() - for field in fields: - if document[field] is None: - continue - if field in schema: - field_schema = schema[field] - field_type = field_schema.get('type') - if field_type is None: - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - schema = {field: optschema} - serialize(document, schema=schema) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - schema = {field: {'type': opttype}} - serialize(document, schema=schema) - if config.AUTO_CREATE_LISTS and field_type == 'list': - # Convert single values to lists - if not isinstance(document[field], list): - document[field] = [document[field]] - if 'schema' in field_schema: - field_schema = field_schema['schema'] - if 'dict' in (field_type, field_schema.get('type')): - # either a dict or a list of dicts - embedded = [document[field]] if field_type == 'dict' \ - else document[field] - for subdocument in embedded: - if type(subdocument) is not dict: - # value is not a dict - continue serialization - # error will be reported by validation if - # appropriate - continue - elif 'schema' in field_schema: - serialize(subdocument, - schema=field_schema['schema']) - else: - serialize(subdocument, schema=field_schema) - elif field_schema.get('type') == 'list': - # a list of lists - sublist_schema = field_schema.get('schema') - item_type = sublist_schema.get('type') - for sublist in document[field]: - for i, v in enumerate(sublist): - if item_type == 'dict': - serialize(sublist[i], - schema=sublist_schema['schema']) - elif item_type in app.data.serializers: - sublist[i] = serialize_value(item_type, v) - elif field_schema.get('type') is None: - # a list of items determined by *of rules - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - schema = {field: { - 'type': field_type, - 'schema': optschema}} - serialize(document, schema=schema) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - schema = {field: { - 'type': field_type, - 'schema': {'type': opttype}}} - serialize(document, schema=schema) - else: - # a list of one type, arbitrary length - field_type = field_schema.get('type') - if field_type in app.data.serializers: - for i, v in enumerate(document[field]): - document[field][i] = \ - serialize_value(field_type, v) - elif 'items' in field_schema: - # a list of multiple types, fixed length - for i, (s, v) in enumerate(zip(field_schema['items'], - document[field])): - field_type = s.get('type') - if field_type in app.data.serializers: - document[field][i] = \ - serialize_value(field_type, document[field][i]) - elif 'valueschema' in field_schema: - # a valueschema - field_type = field_schema['valueschema']['type'] - if field_type == 'objectid': - target = document[field] - for field in target: - target[field] = \ - serialize_value(field_type, target[field]) - elif field_type == 'dict': - for subdocument in document[field].values(): - serialize( - subdocument, - schema=field_schema['valueschema']['schema']) - elif field_type in app.data.serializers: - # a simple field - document[field] = \ - serialize_value(field_type, document[field]) - - return document + """ Recursively handles field values that require data-aware serialization. + Relies on the app.data.serializers dictionary. + + .. versionchanged:: 0.7 + Add support for normalizing anyof-like rules inside lists. See #876. + + .. versionchanged:: 0.6 + Add support for normalizing dotted fields. + + .. versionchanged:: 0.5.4 + Fix serialization of lists of lists. See # 614. + + .. versionchanged:: 0.5.3 + Don't block on custom serialization errors so the whole document can + be processed. See #568. + + .. versionchanged:: 0.5.2 + Fix serialization of keyschemas with objectids. See #525. + + .. versionchanged:: 0.3 + Fix serialization of sub-documents. See #244. + + .. versionadded:: 0.1.1 + """ + + normalize_dotted_fields(document) + + if app.data.serializers: + if resource: + schema = config.DOMAIN[resource]['schema'] + if not fields: + fields = document.keys() + for field in fields: + if document[field] is None: + continue + if field in schema: + field_schema = schema[field] + field_type = field_schema.get('type') + if field_type is None: + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + schema = {field: optschema} + serialize(document, schema=schema) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + schema = {field: {'type': opttype}} + serialize(document, schema=schema) + if config.AUTO_CREATE_LISTS and field_type == 'list': + # Convert single values to lists + if not isinstance(document[field], list): + document[field] = [document[field]] + if 'schema' in field_schema: + field_schema = field_schema['schema'] + if 'dict' in (field_type, field_schema.get('type')): + # either a dict or a list of dicts + embedded = [document[field]] if field_type == 'dict' \ + else document[field] + for subdocument in embedded: + if type(subdocument) is not dict: + # value is not a dict - continue serialization + # error will be reported by validation if + # appropriate + continue + elif 'schema' in field_schema: + serialize(subdocument, + schema=field_schema['schema']) + else: + serialize(subdocument, schema=field_schema) + elif field_schema.get('type') == 'list': + # a list of lists + sublist_schema = field_schema.get('schema') + item_type = sublist_schema.get('type') + for sublist in document[field]: + for i, v in enumerate(sublist): + if item_type == 'dict': + serialize(sublist[i], + schema=sublist_schema['schema']) + elif item_type in app.data.serializers: + sublist[i] = serialize_value(item_type, v) + elif field_schema.get('type') is None: + # a list of items determined by *of rules + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + schema = {field: { + 'type': field_type, + 'schema': optschema}} + serialize(document, schema=schema) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + schema = {field: { + 'type': field_type, + 'schema': {'type': opttype}}} + serialize(document, schema=schema) + else: + # a list of one type, arbitrary length + field_type = field_schema.get('type') + if field_type in app.data.serializers: + for i, v in enumerate(document[field]): + document[field][i] = \ + serialize_value(field_type, v) + elif 'items' in field_schema: + # a list of multiple types, fixed length + for i, (s, v) in enumerate(zip(field_schema['items'], + document[field])): + field_type = s.get('type') + if field_type in app.data.serializers: + document[field][i] = \ + serialize_value(field_type, document[field][i]) + elif 'valueschema' in field_schema: + # a valueschema + field_type = field_schema['valueschema']['type'] + if field_type == 'objectid': + target = document[field] + for field in target: + target[field] = \ + serialize_value(field_type, target[field]) + elif field_type == 'dict': + for subdocument in document[field].values(): + serialize( + subdocument, + schema=field_schema['valueschema']['schema']) + elif field_type in app.data.serializers: + # a simple field + document[field] = \ + serialize_value(field_type, document[field]) + + return document def serialize_value(field_type, value): - """Serialize value of a given type. Relies on the app.data.serializers - dictionary. - """ - try: - return app.data.serializers[field_type](value) - except (KeyError, ValueError, TypeError, InvalidId): - # value can't be cast or no serializer defined, return as is and - # validation will later report back the issue. - return value + """Serialize value of a given type. Relies on the app.data.serializers + dictionary. + """ + try: + return app.data.serializers[field_type](value) + except (KeyError, ValueError, TypeError, InvalidId): + # value can't be cast or no serializer defined, return as is and + # validation will later report back the issue. + return value def normalize_dotted_fields(document): - """ Normalizes eventual dotted fields so validation can be performed - seamlessly. For example this document: + """ Normalizes eventual dotted fields so validation can be performed + seamlessly. For example this document: - {"location.city": "a nested city"} + {"location.city": "a nested city"} - would be normalized to: + would be normalized to: - {"location": {"city": "a nested city"}} + {"location": {"city": "a nested city"}} - Being recursive, normalizing of sub-documents is also supported. For - example: + Being recursive, normalizing of sub-documents is also supported. For + example: - {"location": {"city": "a city", "sub.address": "a subaddress"}} + {"location": {"city": "a city", "sub.address": "a subaddress"}} - would be normalized to: + would be normalized to: - {"location": {"city": "a city", "sub": {"address": "a subaddress}}} + {"location": {"city": "a city", "sub": {"address": "a subaddress}}} - .. versionchanged:: 0.7 - Fix normalization of nested inputs (#738). + .. versionchanged:: 0.7 + Fix normalization of nested inputs (#738). - .. versionadded:: 0.6 - """ - if isinstance(document, list): - prev = document - for i in prev: - normalize_dotted_fields(i) - elif isinstance(document, dict): - for field in list(document): - if '.' in field: - parts = field.split('.') - prev = document - for part in parts[:-1]: - if part not in prev: - prev[part] = {} - prev = prev[part] - if isinstance(document[field], (dict, list)): - normalize_dotted_fields(document[field]) - prev[parts[-1]] = document[field] - document.pop(field) - elif isinstance(document[field], (dict, list)): - normalize_dotted_fields(document[field]) + .. versionadded:: 0.6 + """ + if isinstance(document, list): + prev = document + for i in prev: + normalize_dotted_fields(i) + elif isinstance(document, dict): + for field in list(document): + if '.' in field: + parts = field.split('.') + prev = document + for part in parts[:-1]: + if part not in prev: + prev[part] = {} + prev = prev[part] + if isinstance(document[field], (dict, list)): + normalize_dotted_fields(document[field]) + prev[parts[-1]] = document[field] + document.pop(field) + elif isinstance(document[field], (dict, list)): + normalize_dotted_fields(document[field]) def build_response_document( - document, resource, embedded_fields, latest_doc=None): - """ Prepares a document for response including generation of ETag and - metadata fields. - - :param document: the document to embed other documents into. - :param resource: the resource name. - :param embedded_fields: the list of fields we are allowed to embed. - :param document: the latest version of document. - - .. versionchanged:: 0.5 - Only compute ETAG if necessary (#369). - Add version support (#475). - - .. versionadded:: 0.4 - """ - resource_def = config.DOMAIN[resource] - - # need to update the document field since the etag must be computed on the - # same document representation that might have been used in the collection - # 'get' method - document[config.DATE_CREATED] = date_created(document) - document[config.LAST_UPDATED] = last_updated(document) - - # Up to v0.4 etags were not stored with the documents. - if config.IF_MATCH and config.ETAG not in document: - ignore_fields = resource_def['etag_ignore_fields'] - document[config.ETAG] = document_etag(document, - ignore_fields=ignore_fields) - - # hateoas links - if resource_def['hateoas'] and resource_def['id_field'] in document: - version = None - if resource_def['versioning'] is True \ - and request.args.get(config.VERSION_PARAM): - version = document[config.VERSION] - - self_dict = {'self': document_link(resource, - document[resource_def['id_field']], - version)} - if config.LINKS not in document: - document[config.LINKS] = self_dict - elif 'self' not in document[config.LINKS]: - document[config.LINKS].update(self_dict) - - # add version numbers - resolve_document_version(document, resource, 'GET', latest_doc) - - # resolve media - resolve_media_files(document, resource) - - # resolve soft delete - if resource_def['soft_delete'] is True: - if document.get(config.DELETED) is None: - document[config.DELETED] = False - elif document[config.DELETED] is True: - # Soft deleted documents are sent without expansion of embedded - # documents. Return before resolving them. - return - - # resolve embedded documents - resolve_embedded_documents(document, resource, embedded_fields) + document, resource, embedded_fields, latest_doc=None): + """ Prepares a document for response including generation of ETag and + metadata fields. + + :param document: the document to embed other documents into. + :param resource: the resource name. + :param embedded_fields: the list of fields we are allowed to embed. + :param document: the latest version of document. + + .. versionchanged:: 0.5 + Only compute ETAG if necessary (#369). + Add version support (#475). + + .. versionadded:: 0.4 + """ + resource_def = config.DOMAIN[resource] + + # need to update the document field since the etag must be computed on the + # same document representation that might have been used in the collection + # 'get' method + document[config.DATE_CREATED] = date_created(document) + document[config.LAST_UPDATED] = last_updated(document) + + # Up to v0.4 etags were not stored with the documents. + if config.IF_MATCH and config.ETAG not in document: + ignore_fields = resource_def['etag_ignore_fields'] + document[config.ETAG] = document_etag(document, + ignore_fields=ignore_fields) + + # hateoas links + if resource_def['hateoas'] and resource_def['id_field'] in document: + version = None + if resource_def['versioning'] is True \ + and request.args.get(config.VERSION_PARAM): + version = document[config.VERSION] + + self_dict = {'self': document_link(resource, + document[resource_def['id_field']], + version)} + if config.LINKS not in document: + document[config.LINKS] = self_dict + elif 'self' not in document[config.LINKS]: + document[config.LINKS].update(self_dict) + + # add version numbers + resolve_document_version(document, resource, 'GET', latest_doc) + + # resolve media + resolve_media_files(document, resource) + + # resolve soft delete + if resource_def['soft_delete'] is True: + if document.get(config.DELETED) is None: + document[config.DELETED] = False + elif document[config.DELETED] is True: + # Soft deleted documents are sent without expansion of embedded + # documents. Return before resolving them. + return + + # resolve embedded documents + resolve_embedded_documents(document, resource, embedded_fields) def field_definition(resource, chained_fields): - """ Resolves query string to resource with dot notation like - 'people.address.city' and returns corresponding field definition - of the resource - - :param resource: the resource name whose field to be accepted. - :param chained_fields: query string to retrieve field definition - - .. versionadded 0.5 - """ - definition = config.DOMAIN[resource] - subfields = chained_fields.split('.') - - for field in subfields: - if field not in definition.get('schema', {}): - if 'data_relation' in definition: - sub_resource = definition['data_relation']['resource'] - definition = config.DOMAIN[sub_resource] - - if field not in definition['schema']: - return - definition = definition['schema'][field] - field_type = definition.get('type') - if field_type == 'list': - definition = definition['schema'] - elif field_type == 'objectid': - pass - return definition + """ Resolves query string to resource with dot notation like + 'people.address.city' and returns corresponding field definition + of the resource + + :param resource: the resource name whose field to be accepted. + :param chained_fields: query string to retrieve field definition + + .. versionadded 0.5 + """ + definition = config.DOMAIN[resource] + subfields = chained_fields.split('.') + + for field in subfields: + if field not in definition.get('schema', {}): + if 'data_relation' in definition: + sub_resource = definition['data_relation']['resource'] + definition = config.DOMAIN[sub_resource] + + if field not in definition['schema']: + return + definition = definition['schema'][field] + field_type = definition.get('type') + if field_type == 'list': + definition = definition['schema'] + elif field_type == 'objectid': + pass + return definition def resolve_embedded_fields(resource, req): - """ Returns a list of validated embedded fields from the incoming request - or from the resource definition is the request does not specify. - - :param resource: the resource name. - :param req: and instace of :class:`eve.utils.ParsedRequest`. - - .. versionchanged:: 0.5 - Enables subdocuments embedding. #389. - - .. versionadded:: 0.4 - """ - embedded_fields = [] - non_embedded_fields = [] - if req.embedded: - # Parse the embedded clause, we are expecting - # something like: '{"user":1}' - try: - client_embedding = json.loads(req.embedded) - except ValueError: - abort(400, description='Unable to parse `embedded` clause') - - # Build the list of fields where embedding is being requested - try: - embedded_fields = [k for k, v in client_embedding.items() - if v == 1] - non_embedded_fields = [k for k, v in client_embedding.items() - if v == 0] - except AttributeError: - # We got something other than a dict - abort(400, description='Unable to parse `embedded` clause') - - embedded_fields = list( - (set(config.DOMAIN[resource]['embedded_fields']) | - set(embedded_fields)) - set(non_embedded_fields)) - - # For each field, is the field allowed to be embedded? - # Pick out fields that have a `data_relation` where `embeddable=True` - enabled_embedded_fields = [] - for field in sorted(embedded_fields, key=lambda a: a.count('.')): - # Reject bogus field names - field_def = field_definition(resource, field) - if field_def: - if field_def.get('type') == 'list': - field_def = field_def['schema'] - if 'data_relation' in field_def and \ - field_def['data_relation'].get('embeddable'): - # or could raise 400 here - enabled_embedded_fields.append(field) - - return enabled_embedded_fields + """ Returns a list of validated embedded fields from the incoming request + or from the resource definition is the request does not specify. + + :param resource: the resource name. + :param req: and instace of :class:`eve.utils.ParsedRequest`. + + .. versionchanged:: 0.5 + Enables subdocuments embedding. #389. + + .. versionadded:: 0.4 + """ + embedded_fields = [] + non_embedded_fields = [] + if req.embedded: + # Parse the embedded clause, we are expecting + # something like: '{"user":1}' + try: + client_embedding = json.loads(req.embedded) + except ValueError: + abort(400, description='Unable to parse `embedded` clause') + + # Build the list of fields where embedding is being requested + try: + embedded_fields = [k for k, v in client_embedding.items() + if v == 1] + non_embedded_fields = [k for k, v in client_embedding.items() + if v == 0] + except AttributeError: + # We got something other than a dict + abort(400, description='Unable to parse `embedded` clause') + + embedded_fields = list( + (set(config.DOMAIN[resource]['embedded_fields']) | + set(embedded_fields)) - set(non_embedded_fields)) + + # For each field, is the field allowed to be embedded? + # Pick out fields that have a `data_relation` where `embeddable=True` + enabled_embedded_fields = [] + for field in sorted(embedded_fields, key=lambda a: a.count('.')): + # Reject bogus field names + field_def = field_definition(resource, field) + if field_def: + if field_def.get('type') == 'list': + field_def = field_def['schema'] + if 'data_relation' in field_def and \ + field_def['data_relation'].get('embeddable'): + # or could raise 400 here + enabled_embedded_fields.append(field) + + return enabled_embedded_fields def embedded_document(reference, data_relation, field_name): - """ Returns a document to be embedded by reference using data_relation - taking into account document versions - - :param reference: reference to the document to be embedded. - :param data_relation: the relation schema definition. - :param field_name: field name used in abort message only - - .. versionadded:: 0.5 - """ - # Retrieve and serialize the requested document - if 'version' in data_relation and data_relation['version'] is True: - # grab the specific version - embedded_doc = get_data_version_relation_document( - data_relation, reference) - - # grab the latest version - latest_embedded_doc = get_data_version_relation_document( - data_relation, reference, latest=True) - - # make sure we got the documents - if embedded_doc is None or latest_embedded_doc is None: - # your database is not consistent!!! that is bad - # TODO: we should notify the developers with a log. - abort(404, description=debug_error_message( - "Unable to locate embedded documents for '%s'" % - field_name - )) - - build_response_document(embedded_doc, data_relation['resource'], - [], latest_embedded_doc) - else: - # if reference is DBRef take the referenced collection as subresource - subresource = reference.collection if isinstance(reference, DBRef) \ - else data_relation['resource'] - id_field = config.DOMAIN[subresource]['id_field'] - embedded_doc = app.data.find_one(subresource, None, - **{id_field: reference.id - if isinstance(reference, DBRef) - else reference}) - if embedded_doc: - resolve_media_files(embedded_doc, subresource) - - return embedded_doc + """ Returns a document to be embedded by reference using data_relation + taking into account document versions + + :param reference: reference to the document to be embedded. + :param data_relation: the relation schema definition. + :param field_name: field name used in abort message only + + .. versionadded:: 0.5 + """ + # Retrieve and serialize the requested document + if 'version' in data_relation and data_relation['version'] is True: + # grab the specific version + embedded_doc = get_data_version_relation_document( + data_relation, reference) + + # grab the latest version + latest_embedded_doc = get_data_version_relation_document( + data_relation, reference, latest=True) + + # make sure we got the documents + if embedded_doc is None or latest_embedded_doc is None: + # your database is not consistent!!! that is bad + # TODO: we should notify the developers with a log. + abort(404, description=debug_error_message( + "Unable to locate embedded documents for '%s'" % + field_name + )) + + build_response_document(embedded_doc, data_relation['resource'], + [], latest_embedded_doc) + else: + # if reference is DBRef take the referenced collection as subresource + subresource = reference.collection if isinstance(reference, DBRef) \ + else data_relation['resource'] + id_field = config.DOMAIN[subresource]['id_field'] + embedded_doc = app.data.find_one(subresource, None, + **{id_field: reference.id + if isinstance(reference, DBRef) + else reference}) + if embedded_doc: + resolve_media_files(embedded_doc, subresource) + + return embedded_doc def subdocuments(fields_chain, resource, document): - """ Traverses the given document and yields subdocuments which - correspond to the given fields_chain - - :param fields_chain: list of nested field names. - :param resource: the resource name. - :param document: document to be traversed - - .. versionadded:: 0.5 - """ - if len(fields_chain) == 0: - yield document - elif isinstance(document, dict) and fields_chain[0] in document: - subdocument = document[fields_chain[0]] - docs = subdocument if isinstance(subdocument, list) else [subdocument] - try: - resource = field_definition( - resource, fields_chain[0])['data_relation']['resource'] - except KeyError: - resource = resource - - for doc in docs: - for result in subdocuments(fields_chain[1:], resource, doc): - yield result - else: - yield document + """ Traverses the given document and yields subdocuments which + correspond to the given fields_chain + + :param fields_chain: list of nested field names. + :param resource: the resource name. + :param document: document to be traversed + + .. versionadded:: 0.5 + """ + if len(fields_chain) == 0: + yield document + elif isinstance(document, dict) and fields_chain[0] in document: + subdocument = document[fields_chain[0]] + docs = subdocument if isinstance(subdocument, list) else [subdocument] + try: + resource = field_definition( + resource, fields_chain[0])['data_relation']['resource'] + except KeyError: + resource = resource + + for doc in docs: + for result in subdocuments(fields_chain[1:], resource, doc): + yield result + else: + yield document def resolve_embedded_documents(document, resource, embedded_fields): - """ Loops through the documents, adding embedded representations - of any fields that are (1) defined eligible for embedding in the - DOMAIN and (2) requested to be embedded in the current `req`. - - Currently we support embedding of documents by references located - in any subdocuments. For example, query embedded={"user.friends":1} - will return a document with "user" and all his "friends" embedded, - but only if "user" is a subdocument. - - We do not support multiple layers embeddings. - - :param document: the document to embed other documents into. - :param resource: the resource name. - :param embedded_fields: the list of fields we are allowed to embed. - - .. versionchanged:: 0.5 - Support for embedding documents located in subdocuments. - Allocated two functions embedded_document and subdocuments. - - .. versionchanged:: 0.4 - Moved parsing of embedded fields to _resolve_embedded_fields. - Support for document versioning. - - .. versionchanged:: 0.2 - Support for 'embedded_fields'. - - .. versionchanged:: 0.1.1 - 'collection' key has been renamed to 'resource' (data_relation). - - .. versionadded:: 0.1.0 - """ - # NOTE(Gonéri): We resolve the embedded documents at the end. - for field in sorted(embedded_fields, key=lambda a: a.count('.')): - data_relation = field_definition(resource, field)['data_relation'] - getter = lambda ref: embedded_document(ref, data_relation, field) # noqa - fields_chain = field.split('.') - last_field = fields_chain[-1] - for subdocument in subdocuments(fields_chain[:-1], resource, document): - if last_field not in subdocument: - continue - if isinstance(subdocument[last_field], list): - subdocument[last_field] = list(map(getter, - subdocument[last_field])) - else: - subdocument[last_field] = getter(subdocument[last_field]) + """ Loops through the documents, adding embedded representations + of any fields that are (1) defined eligible for embedding in the + DOMAIN and (2) requested to be embedded in the current `req`. + + Currently we support embedding of documents by references located + in any subdocuments. For example, query embedded={"user.friends":1} + will return a document with "user" and all his "friends" embedded, + but only if "user" is a subdocument. + + We do not support multiple layers embeddings. + + :param document: the document to embed other documents into. + :param resource: the resource name. + :param embedded_fields: the list of fields we are allowed to embed. + + .. versionchanged:: 0.5 + Support for embedding documents located in subdocuments. + Allocated two functions embedded_document and subdocuments. + + .. versionchanged:: 0.4 + Moved parsing of embedded fields to _resolve_embedded_fields. + Support for document versioning. + + .. versionchanged:: 0.2 + Support for 'embedded_fields'. + + .. versionchanged:: 0.1.1 + 'collection' key has been renamed to 'resource' (data_relation). + + .. versionadded:: 0.1.0 + """ + # NOTE(Gonéri): We resolve the embedded documents at the end. + for field in sorted(embedded_fields, key=lambda a: a.count('.')): + data_relation = field_definition(resource, field)['data_relation'] + getter = lambda ref: embedded_document(ref, data_relation, field) # noqa + fields_chain = field.split('.') + last_field = fields_chain[-1] + for subdocument in subdocuments(fields_chain[:-1], resource, document): + if last_field not in subdocument: + continue + if isinstance(subdocument[last_field], list): + subdocument[last_field] = list(map(getter, + subdocument[last_field])) + else: + subdocument[last_field] = getter(subdocument[last_field]) def resolve_media_files(document, resource): - """ Embed media files into the response document. + """ Embed media files into the response document. - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. - .. versionadded:: 0.4 - """ - for field in resource_media_fields(document, resource): - if isinstance(document[field], list): - resolved_list = [] - for file_id in document[field]: - resolved_list.append(resolve_one_media(file_id, resource)) - document[field] = resolved_list - else: - document[field] = resolve_one_media(document[field], resource) + .. versionadded:: 0.4 + """ + for field in resource_media_fields(document, resource): + if isinstance(document[field], list): + resolved_list = [] + for file_id in document[field]: + resolved_list.append(resolve_one_media(file_id, resource)) + document[field] = resolved_list + else: + document[field] = resolve_one_media(document[field], resource) def resolve_one_media(file_id, resource): - """ Get response for one media file """ - _file = app.media.get(file_id, resource) - - if _file: - # otherwise we have a valid file and should send extended response - # start with the basic file object - if config.RETURN_MEDIA_AS_BASE64_STRING: - ret_file = base64.encodestring(_file.read()) - elif config.RETURN_MEDIA_AS_URL: - prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ - is not None else app.api_prefix - ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, - file_id) - else: - ret_file = None - - if config.EXTENDED_MEDIA_INFO: - ret = { - 'file': ret_file, - } - - # check if we should return any special fields - for attribute in config.EXTENDED_MEDIA_INFO: - if hasattr(_file, attribute): - # add extended field if found in the file object - ret.update({ - attribute: getattr(_file, attribute) - }) - else: - # tried to select an invalid attribute - abort(500, description=debug_error_message( - 'Invalid extended media attribute requested' - )) - - return ret - else: - return ret_file - else: - return None + """ Get response for one media file """ + _file = app.media.get(file_id, resource) + + if _file: + # otherwise we have a valid file and should send extended response + # start with the basic file object + if config.RETURN_MEDIA_AS_BASE64_STRING: + ret_file = base64.encodestring(_file.read()) + elif config.RETURN_MEDIA_AS_URL: + prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ + is not None else app.api_prefix + ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, + file_id) + else: + ret_file = None + + if config.EXTENDED_MEDIA_INFO: + ret = { + 'file': ret_file, + } + + # check if we should return any special fields + for attribute in config.EXTENDED_MEDIA_INFO: + if hasattr(_file, attribute): + # add extended field if found in the file object + ret.update({ + attribute: getattr(_file, attribute) + }) + else: + # tried to select an invalid attribute + abort(500, description=debug_error_message( + 'Invalid extended media attribute requested' + )) + + return ret + else: + return ret_file + else: + return None def marshal_write_response(document, resource): - """ Limit response document to minimize bandwidth when client supports it. - - :param document: the response document. - :param resource: the resource being consumed by the request. - - .. versionchanged: 0.5 - Avoid exposing 'auth_field' if it is not intended to be public. - - .. versionadded:: 0.4 - """ - - resource_def = app.config['DOMAIN'][resource] - if app.config['BANDWIDTH_SAVER'] is True: - # only return the automatic fields and special extra fields - fields = auto_fields(resource) + resource_def['extra_response_fields'] - document = dict((k, v) for (k, v) in document.items() if k in fields) - else: - # avoid exposing the auth_field if it is not included in the - # resource schema. - auth_field = resource_def.get('auth_field') - if auth_field and auth_field not in resource_def['schema']: - try: - del(document[auth_field]) - except: - # 'auth_field' value has not been set by the auth class. - pass - return document + """ Limit response document to minimize bandwidth when client supports it. + + :param document: the response document. + :param resource: the resource being consumed by the request. + + .. versionchanged: 0.5 + Avoid exposing 'auth_field' if it is not intended to be public. + + .. versionadded:: 0.4 + """ + + resource_def = app.config['DOMAIN'][resource] + if app.config['BANDWIDTH_SAVER'] is True: + # only return the automatic fields and special extra fields + fields = auto_fields(resource) + resource_def['extra_response_fields'] + document = dict((k, v) for (k, v) in document.items() if k in fields) + else: + # avoid exposing the auth_field if it is not included in the + # resource schema. + auth_field = resource_def.get('auth_field') + if auth_field and auth_field not in resource_def['schema']: + try: + del (document[auth_field]) + except: + # 'auth_field' value has not been set by the auth class. + pass + return document def store_media_files(document, resource, original=None): - """ Store any media file in the underlying media store and update the - document with unique ids of stored files. - - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. - :param original: original document being replaced or edited. - - .. versionchanged:: 0.4 - Renamed to store_media_files to deconflict with new resolve_media_files. - - .. versionadded:: 0.3 - """ - # TODO We're storing media files in advance, before the corresponding - # document is also stored. In the rare occurrence that the subsequent - # document update fails we should probably attempt a cleanup on the storage - # system. Easier said than done though. - for field in resource_media_fields(document, resource): - if original and field in original: - # since file replacement is not supported by the media storage - # system, we first need to delete the files being replaced. - if isinstance(original[field], list): - for file_id in original[field]: - app.media.delete(file_id, resource) - else: - app.media.delete(original[field], resource) - - if document[field]: - # store files and update document with file's unique id/filename - # also pass in mimetype for use when retrieving the file - if isinstance(document[field], list): - id_lst = [] - for stor_obj in document[field]: - id_lst.append(app.media.put( - stor_obj, filename=stor_obj.filename, - content_type=stor_obj.mimetype, resource=resource)) - document[field] = id_lst - else: - document[field] = app.media.put( - document[field], filename=document[field].filename, - content_type=document[field].mimetype, resource=resource) + """ Store any media file in the underlying media store and update the + document with unique ids of stored files. + + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. + :param original: original document being replaced or edited. + + .. versionchanged:: 0.4 + Renamed to store_media_files to deconflict with new resolve_media_files. + + .. versionadded:: 0.3 + """ + # TODO We're storing media files in advance, before the corresponding + # document is also stored. In the rare occurrence that the subsequent + # document update fails we should probably attempt a cleanup on the storage + # system. Easier said than done though. + for field in resource_media_fields(document, resource): + if original and field in original: + # since file replacement is not supported by the media storage + # system, we first need to delete the files being replaced. + if isinstance(original[field], list): + for file_id in original[field]: + app.media.delete(file_id, resource) + else: + app.media.delete(original[field], resource) + + if document[field]: + # store files and update document with file's unique id/filename + # also pass in mimetype for use when retrieving the file + if isinstance(document[field], list): + id_lst = [] + for stor_obj in document[field]: + id_lst.append(app.media.put( + stor_obj, filename=stor_obj.filename, + content_type=stor_obj.mimetype, resource=resource)) + document[field] = id_lst + else: + document[field] = app.media.put( + document[field], filename=document[field].filename, + content_type=document[field].mimetype, resource=resource) def resource_media_fields(document, resource): - """ Returns a list of media fields defined in the resource schema. + """ Returns a list of media fields defined in the resource schema. - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. - .. versionadded:: 0.3 - """ - media_fields = app.config['DOMAIN'][resource]['_media'] - return [field for field in media_fields if field in document] + .. versionadded:: 0.3 + """ + media_fields = app.config['DOMAIN'][resource]['_media'] + return [field for field in media_fields if field in document] def resolve_sub_resource_path(document, resource): - if not request.view_args: - return + if not request.view_args: + return - resource_def = config.DOMAIN[resource] - schema = resource_def['schema'] - fields = [] - for field, value in request.view_args.items(): - if field in schema and field != resource_def['id_field']: - fields.append(field) - document[field] = value + resource_def = config.DOMAIN[resource] + schema = resource_def['schema'] + fields = [] + for field, value in request.view_args.items(): + if field in schema and field != resource_def['id_field']: + fields.append(field) + document[field] = value - if fields: - serialize(document, resource, fields=fields) + if fields: + serialize(document, resource, fields=fields) def resolve_user_restricted_access(document, resource): - """ Adds user restricted access metadata to the document if applicable. + """ Adds user restricted access metadata to the document if applicable. - :param document: the document being posted or replaced - :param resource: the resource to which the document belongs + :param document: the document being posted or replaced + :param resource: the resource to which the document belongs - .. versionchanged:: 0.5.2 - Make User Restricted Resource Access work with HMAC Auth too. + .. versionchanged:: 0.5.2 + Make User Restricted Resource Access work with HMAC Auth too. - .. versionchanged:: 0.4 - Use new auth.request_auth_value() method. + .. versionchanged:: 0.4 + Use new auth.request_auth_value() method. - .. versionadded:: 0.3 - """ - # if 'user-restricted resource access' is enabled and there's - # an Auth request active, inject the username into the document - resource_def = app.config['DOMAIN'][resource] - auth = resource_def['authentication'] - auth_field = resource_def['auth_field'] - if auth and auth_field: - request_auth_value = auth.get_request_auth_value() - if request_auth_value: - document[auth_field] = request_auth_value + .. versionadded:: 0.3 + """ + # if 'user-restricted resource access' is enabled and there's + # an Auth request active, inject the username into the document + resource_def = app.config['DOMAIN'][resource] + auth = resource_def['authentication'] + auth_field = resource_def['auth_field'] + if auth and auth_field: + request_auth_value = auth.get_request_auth_value() + if request_auth_value: + document[auth_field] = request_auth_value def resolve_document_etag(documents, resource): - """ Adds etags to documents. + """ Adds etags to documents. - .. versionadded:: 0.5 - """ - if config.IF_MATCH: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] + .. versionadded:: 0.5 + """ + if config.IF_MATCH: + ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] - if not isinstance(documents, list): - documents = [documents] + if not isinstance(documents, list): + documents = [documents] - for document in documents: - document[config.ETAG] =\ - document_etag(document, ignore_fields=ignore_fields) + for document in documents: + document[config.ETAG] = \ + document_etag(document, ignore_fields=ignore_fields) def pre_event(f): - """ Enable a Hook pre http request. - - .. versionchanged:: 0.6 - Enable callback hooks for HEAD requests. - - .. versionchanged:: 0.4 - Merge 'sub_resource_lookup' (args[1]) with kwargs, so http methods can - all enjoy the same signature, and data layer find methods can seemingly - process both kind of queries. - - .. versionadded:: 0.2 - """ - @wraps(f) - def decorated(*args, **kwargs): - method = request.method - if method == 'HEAD': - method = 'GET' - - event_name = 'on_pre_' + method - resource = args[0] if args else None - gh_params = () - rh_params = () - if method in ('GET', 'PATCH', 'DELETE', 'PUT'): - gh_params = (resource, request, kwargs) - rh_params = (request, kwargs) - elif method in ('POST', ): - # POST hook does not support the kwargs argument - gh_params = (resource, request) - rh_params = (request,) - - # general hook - getattr(app, event_name)(*gh_params) - if resource: - # resource hook - getattr(app, event_name + '_' + resource)(*rh_params) - - combined_args = kwargs - if len(args) > 1: - combined_args.update(args[1].items()) - r = f(resource, **combined_args) - return r - return decorated + """ Enable a Hook pre http request. + + .. versionchanged:: 0.6 + Enable callback hooks for HEAD requests. + + .. versionchanged:: 0.4 + Merge 'sub_resource_lookup' (args[1]) with kwargs, so http methods can + all enjoy the same signature, and data layer find methods can seemingly + process both kind of queries. + + .. versionadded:: 0.2 + """ + + @wraps(f) + def decorated(*args, **kwargs): + method = request.method + if method == 'HEAD': + method = 'GET' + + event_name = 'on_pre_' + method + resource = args[0] if args else None + gh_params = () + rh_params = () + if method in ('GET', 'PATCH', 'DELETE', 'PUT'): + gh_params = (resource, request, kwargs) + rh_params = (request, kwargs) + elif method in ('POST',): + # POST hook does not support the kwargs argument + gh_params = (resource, request) + rh_params = (request,) + + # general hook + getattr(app, event_name)(*gh_params) + if resource: + # resource hook + getattr(app, event_name + '_' + resource)(*rh_params) + + combined_args = kwargs + if len(args) > 1: + combined_args.update(args[1].items()) + r = f(resource, **combined_args) + return r + + return decorated def document_link(resource, document_id, version=None): - """ Returns a link to a document endpoint. + """ Returns a link to a document endpoint. - :param resource: the resource name. - :param document_id: the document unique identifier. - :param version: the document version. Defaults to None. + :param resource: the resource name. + :param document_id: the document unique identifier. + :param version: the document version. Defaults to None. - .. versionchanged:: 0.5 - Add version support (#475). + .. versionchanged:: 0.5 + Add version support (#475). - .. versionchanged:: 0.4 - Use the regex-neutral resource_link function. + .. versionchanged:: 0.4 + Use the regex-neutral resource_link function. - .. versionchanged:: 0.1.0 - No more trailing slashes in links. + .. versionchanged:: 0.1.0 + No more trailing slashes in links. - .. versionchanged:: 0.0.3 - Now returning a JSON link - """ - version_part = '?version=%s' % version if version else '' - return {'title': '%s' % config.DOMAIN[resource]['item_title'], - 'href': '%s/%s%s' % (resource_link(), document_id, version_part)} + .. versionchanged:: 0.0.3 + Now returning a JSON link + """ + version_part = '?version=%s' % version if version else '' + return {'title': '%s' % config.DOMAIN[resource]['item_title'], + 'href': '%s/%s%s' % (resource_link(), document_id, version_part)} def resource_link(): - """ Returns the current resource path relative to the API entry point. - Mostly going to be used by hateoas functions when building - document/resource links. The resource URL stored in the config settings - might contain regexes and custom variable names, all of which are not - needed in the response payload. + """ Returns the current resource path relative to the API entry point. + Mostly going to be used by hateoas functions when building + document/resource links. The resource URL stored in the config settings + might contain regexes and custom variable names, all of which are not + needed in the response payload. - .. versionchanged:: 0.5 - URL is relative to API root. + .. versionchanged:: 0.5 + URL is relative to API root. - .. versionadded:: 0.4 - """ - path = request.path.strip('/') + .. versionadded:: 0.4 + """ + path = request.path.strip('/') - if '|item' in request.endpoint: - path = path[:path.rfind('/')] + if '|item' in request.endpoint: + path = path[:path.rfind('/')] - def strip_prefix(hit): - return path[len(hit):] if path.startswith(hit) else path + def strip_prefix(hit): + return path[len(hit):] if path.startswith(hit) else path - if config.URL_PREFIX: - path = strip_prefix(config.URL_PREFIX + '/') - if config.API_VERSION: - path = strip_prefix(config.API_VERSION + '/') - return path + if config.URL_PREFIX: + path = strip_prefix(config.URL_PREFIX + '/') + if config.API_VERSION: + path = strip_prefix(config.API_VERSION + '/') + return path def oplog_push(resource, document, op, id=None): - """ Pushes an edit operation to the oplog if included in OPLOG_METHODS. To - save on storage space (at least on MongoDB) field names are shortened: - - 'r' = resource endpoint, - 'o' = operation performed, - 'i' = unique id of the document involved, - 'pi' = client IP, - 'c' = changes - - config.LAST_UPDATED, config.LAST_CREATED and AUTH_FIELD are not being - shortened to allow for standard endpoint behavior (so clients can - query the endpoint with If-Modified-Since queries, and User-Restricted- - Resource-Access will keep working on the oplog endpoint too). - - :param resource: name of the resource involved. - :param document: updates performed with the edit operation. - :param op: operation performed. Can be 'POST', 'PUT', 'PATCH', 'DELETE'. - :param id: unique id of the document. - - .. versionchanged:: 0.7 - Add user information to the audit. Closes #846. - Raise on_oplog_push event. - Add support for 'extra' custom field. - - .. versionchanged:: 0.5.4 - Use a copy of original document in order to avoid altering its state. - See #590. - - .. versionadded:: 0.5 - """ - if not config.OPLOG or op not in config.OPLOG_METHODS or resource not in config.URLS: - return - - resource_def = config.DOMAIN[resource] - - if document is None: - updates = {} - else: - updates = copy(document) - - if not isinstance(updates, list): - updates = [updates] - - entries = [] - for update in updates: - entry = { - 'r': config.URLS[resource], - 'o': op, - 'i': (update[resource_def['id_field']] - if resource_def['id_field'] in update else id), - } - if config.LAST_UPDATED in update: - last_update = update[config.LAST_UPDATED] - else: - last_update = datetime.utcnow().replace(microsecond=0) - entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update - if config.OPLOG_AUDIT: - entry['ip'] = request.remote_addr - - auth = resource_def['authentication'] - entry['u'] = auth.get_user_or_token() if auth else 'n/a' - - if op in config.OPLOG_CHANGE_METHODS: - # these fields are already contained in 'entry'. - del(update[config.LAST_UPDATED]) - # legacy documents (v0.4 or less) could be missing the etag - # field - if config.ETAG in update: - del(update[config.ETAG]) - entry['c'] = update - else: - pass - - resolve_user_restricted_access(entry, config.OPLOG_NAME) - - entries.append(entry) - - if entries: - # notify callbacks - getattr(app, "on_oplog_push")(resource, entries) - # oplog push - app.data.insert(config.OPLOG_NAME, entries) + """ Pushes an edit operation to the oplog if included in OPLOG_METHODS. To + save on storage space (at least on MongoDB) field names are shortened: + + 'r' = resource endpoint, + 'o' = operation performed, + 'i' = unique id of the document involved, + 'pi' = client IP, + 'c' = changes + + config.LAST_UPDATED, config.LAST_CREATED and AUTH_FIELD are not being + shortened to allow for standard endpoint behavior (so clients can + query the endpoint with If-Modified-Since queries, and User-Restricted- + Resource-Access will keep working on the oplog endpoint too). + + :param resource: name of the resource involved. + :param document: updates performed with the edit operation. + :param op: operation performed. Can be 'POST', 'PUT', 'PATCH', 'DELETE'. + :param id: unique id of the document. + + .. versionchanged:: 0.7 + Add user information to the audit. Closes #846. + Raise on_oplog_push event. + Add support for 'extra' custom field. + + .. versionchanged:: 0.5.4 + Use a copy of original document in order to avoid altering its state. + See #590. + + .. versionadded:: 0.5 + """ + if not config.OPLOG \ + or op not in config.OPLOG_METHODS \ + or resource not in config.URLS: + return + + resource_def = config.DOMAIN[resource] + + if document is None: + updates = {} + else: + updates = copy(document) + + if not isinstance(updates, list): + updates = [updates] + + entries = [] + for update in updates: + entry = { + 'r': config.URLS[resource], + 'o': op, + 'i': (update[resource_def['id_field']] + if resource_def['id_field'] in update else id), + } + if config.LAST_UPDATED in update: + last_update = update[config.LAST_UPDATED] + else: + last_update = datetime.utcnow().replace(microsecond=0) + entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update + if config.OPLOG_AUDIT: + entry['ip'] = request.remote_addr + + auth = resource_def['authentication'] + entry['u'] = auth.get_user_or_token() if auth else 'n/a' + + if op in config.OPLOG_CHANGE_METHODS: + # these fields are already contained in 'entry'. + del (update[config.LAST_UPDATED]) + # legacy documents (v0.4 or less) could be missing the etag + # field + if config.ETAG in update: + del (update[config.ETAG]) + entry['c'] = update + else: + pass + + resolve_user_restricted_access(entry, config.OPLOG_NAME) + + entries.append(entry) + + if entries: + # notify callbacks + getattr(app, "on_oplog_push")(resource, entries) + # oplog push + app.data.insert(config.OPLOG_NAME, entries) From 6629241adb3c3615a0214fb2aa3f6da3929feb44 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Thu, 20 Apr 2017 21:54:49 +0200 Subject: [PATCH 159/821] Reverted autoformat and ran flake8 --- eve/methods/common.py | 2006 ++++++++++++++++++++--------------------- 1 file changed, 1000 insertions(+), 1006 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 73704bc5d..58aa60f68 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -34,1153 +34,1147 @@ def get_document(resource, concurrency_check, **lookup): - """ Retrieves and return a single document. Since this function is used by - the editing methods (PUT, PATCH, DELETE), we make sure that the client - request references the current representation of the document before - returning it. However, this concurrency control may be turned off by - internal functions. If resource enables soft delete, soft deleted documents - will be returned, and must be handled by callers. - - :param resource: the name of the resource to which the document belongs to. - :param concurrency_check: boolean check for concurrency control - :param **lookup: document lookup query - - .. versionchanged:: 0.6 - Return soft deleted documents. - - .. versionchanged:: 0.5 - Concurrency control optional for internal functions. - ETAG are now stored with the document (#369). - - .. versionchanged:: 0.0.9 - More informative error messages. - - .. versionchanged:: 0.0.5 - Pass current resource to ``parse_request``, allowing for proper - processing of new configuration settings: `filters`, `sorting`, `paging`. - """ - req = parse_request(resource) - if config.DOMAIN[resource]['soft_delete']: - # get_document should always fetch soft deleted documents from the db - # callers must handle soft deleted documents - req.show_deleted = True - - document = app.data.find_one(resource, req, **lookup) - if document: - e_if_m = config.ENFORCE_IF_MATCH - if_m = config.IF_MATCH - if not req.if_match and e_if_m and if_m and concurrency_check: - # we don't allow editing unless the client provides an etag - # for the document or explicitly decides to allow editing by either - # disabling the ``concurrency_check`` or ``IF_MATCH`` or - # ``ENFORCE_IF_MATCH`` fields. - abort(428, description='To edit a document ' - 'its etag must be provided using the If-Match header') - - # ensure the retrieved document has LAST_UPDATED and DATE_CREATED, - # eventually with same default values as in GET. - document[config.LAST_UPDATED] = last_updated(document) - document[config.DATE_CREATED] = date_created(document) - - if req.if_match and concurrency_check: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] - etag = document.get(config.ETAG, document_etag(document, - ignore_fields=ignore_fields)) - if req.if_match != etag: - # client and server etags must match, or we don't allow editing - # (ensures that client's version of the document is up to date) - abort(412, description='Client and server etags don\'t match') - - return document + """ Retrieves and return a single document. Since this function is used by + the editing methods (PUT, PATCH, DELETE), we make sure that the client + request references the current representation of the document before + returning it. However, this concurrency control may be turned off by + internal functions. If resource enables soft delete, soft deleted documents + will be returned, and must be handled by callers. + + :param resource: the name of the resource to which the document belongs to. + :param concurrency_check: boolean check for concurrency control + :param **lookup: document lookup query + + .. versionchanged:: 0.6 + Return soft deleted documents. + + .. versionchanged:: 0.5 + Concurrency control optional for internal functions. + ETAG are now stored with the document (#369). + + .. versionchanged:: 0.0.9 + More informative error messages. + + .. versionchanged:: 0.0.5 + Pass current resource to ``parse_request``, allowing for proper + processing of new configuration settings: `filters`, `sorting`, `paging`. + """ + req = parse_request(resource) + if config.DOMAIN[resource]['soft_delete']: + # get_document should always fetch soft deleted documents from the db + # callers must handle soft deleted documents + req.show_deleted = True + + document = app.data.find_one(resource, req, **lookup) + if document: + e_if_m = config.ENFORCE_IF_MATCH + if_m = config.IF_MATCH + if not req.if_match and e_if_m and if_m and concurrency_check: + # we don't allow editing unless the client provides an etag + # for the document or explicitly decides to allow editing by either + # disabling the ``concurrency_check`` or ``IF_MATCH`` or + # ``ENFORCE_IF_MATCH`` fields. + abort(428, description='To edit a document ' + 'its etag must be provided using the If-Match header') + + # ensure the retrieved document has LAST_UPDATED and DATE_CREATED, + # eventually with same default values as in GET. + document[config.LAST_UPDATED] = last_updated(document) + document[config.DATE_CREATED] = date_created(document) + + if req.if_match and concurrency_check: + ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] + etag = document.get(config.ETAG, document_etag(document, + ignore_fields=ignore_fields)) + if req.if_match != etag: + # client and server etags must match, or we don't allow editing + # (ensures that client's version of the document is up to date) + abort(412, description='Client and server etags don\'t match') + + return document def parse(value, resource): - """ Safely evaluates a string containing a Python expression. We are - receiving json and returning a dict. + """ Safely evaluates a string containing a Python expression. We are + receiving json and returning a dict. - :param value: the string to be evaluated. - :param resource: name of the involved resource. + :param value: the string to be evaluated. + :param resource: name of the involved resource. - .. versionchanged:: 0.1.1 - Serialize data-specific values as needed. + .. versionchanged:: 0.1.1 + Serialize data-specific values as needed. - .. versionchanged:: 0.1.0 - Support for PUT method. + .. versionchanged:: 0.1.0 + Support for PUT method. - .. versionchanged:: 0.0.5 - Support for 'application/json' Content-Type. + .. versionchanged:: 0.0.5 + Support for 'application/json' Content-Type. - .. versionchanged:: 0.0.4 - When parsing POST requests, eventual default values are injected in - parsed documents. - """ + .. versionchanged:: 0.0.4 + When parsing POST requests, eventual default values are injected in + parsed documents. + """ - try: - # assume it's not decoded to json yet (request Content-Type = form) - document = json.loads(value) - except: - # already a json - document = value + try: + # assume it's not decoded to json yet (request Content-Type = form) + document = json.loads(value) + except: + # already a json + document = value - # if needed, get field values serialized by the data diver being used. - # If any error occurs, assume validation will take care of it (i.e. a badly - # formatted objectid). - try: - document = serialize(document, resource) - except: - pass + # if needed, get field values serialized by the data diver being used. + # If any error occurs, assume validation will take care of it (i.e. a badly + # formatted objectid). + try: + document = serialize(document, resource) + except: + pass - return document + return document def payload(): - """ Performs sanity checks or decoding depending on the Content-Type, - then returns the request payload as a dict. If request Content-Type is - unsupported, aborts with a 400 (Bad Request). - - .. versionchanged:: 0.7 - Allow 'multipart/form-data' form fields to be JSON encoded, once the - MULTIPART_FORM_FIELDS_AS_JSON setting was been set. - - .. versionchanged:: 0.3 - Allow 'multipart/form-data' content type. - - .. versionchanged:: 0.1.1 - Payload returned as a standard python dict regardless of request content - type. - - .. versionchanged:: 0.0.9 - More informative error messages. - request.get_json() replaces the now deprecated request.json - - - .. versionchanged:: 0.0.7 - Native Flask request.json preferred over json.loads. - - .. versionadded: 0.0.5 - """ - content_type = request.headers.get('Content-Type', '').split(';')[0] - - if content_type == 'application/json': - return request.get_json() - elif content_type == 'application/x-www-form-urlencoded': - return multidict_to_dict(request.form) if len(request.form) else \ - abort(400, description='No form-urlencoded data supplied') - elif content_type == 'multipart/form-data': - # as multipart is also used for file uploads, we let an empty - # request.form go through as long as there are also files in the - # request. - if len(request.form) or len(request.files): - # merge form fields and request files, so we get a single payload - # to be validated against the resource schema. - - formItems = MultiDict(request.form) - - if config.MULTIPART_FORM_FIELDS_AS_JSON: - for key, lst in formItems.lists(): - new_lst = [] - for value in lst: - try: - new_lst.append(json.loads(value)) - except ValueError: - new_lst.append(json.loads('"{0}"'.format(value))) - formItems.setlist(key, new_lst) - - payload = CombinedMultiDict([formItems, request.files]) - return multidict_to_dict(payload) - - else: - abort(400, description='No multipart/form-data supplied') - else: - abort(400, description='Unknown or no Content-Type header supplied') + """ Performs sanity checks or decoding depending on the Content-Type, + then returns the request payload as a dict. If request Content-Type is + unsupported, aborts with a 400 (Bad Request). + + .. versionchanged:: 0.7 + Allow 'multipart/form-data' form fields to be JSON encoded, once the + MULTIPART_FORM_FIELDS_AS_JSON setting was been set. + + .. versionchanged:: 0.3 + Allow 'multipart/form-data' content type. + + .. versionchanged:: 0.1.1 + Payload returned as a standard python dict regardless of request content + type. + + .. versionchanged:: 0.0.9 + More informative error messages. + request.get_json() replaces the now deprecated request.json + + + .. versionchanged:: 0.0.7 + Native Flask request.json preferred over json.loads. + + .. versionadded: 0.0.5 + """ + content_type = request.headers.get('Content-Type', '').split(';')[0] + + if content_type == 'application/json': + return request.get_json() + elif content_type == 'application/x-www-form-urlencoded': + return multidict_to_dict(request.form) if len(request.form) else \ + abort(400, description='No form-urlencoded data supplied') + elif content_type == 'multipart/form-data': + # as multipart is also used for file uploads, we let an empty + # request.form go through as long as there are also files in the + # request. + if len(request.form) or len(request.files): + # merge form fields and request files, so we get a single payload + # to be validated against the resource schema. + + formItems = MultiDict(request.form) + + if config.MULTIPART_FORM_FIELDS_AS_JSON: + for key, lst in formItems.lists(): + new_lst = [] + for value in lst: + try: + new_lst.append(json.loads(value)) + except ValueError: + new_lst.append(json.loads('"{0}"'.format(value))) + formItems.setlist(key, new_lst) + + payload = CombinedMultiDict([formItems, request.files]) + return multidict_to_dict(payload) + + else: + abort(400, description='No multipart/form-data supplied') + else: + abort(400, description='Unknown or no Content-Type header supplied') def multidict_to_dict(multidict): - """ Convert a MultiDict containing form data into a regular dict. If the - config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the - same key get entered as a list. If it is False, the first entry is picked. - """ - if config.AUTO_COLLAPSE_MULTI_KEYS: - d = dict(multidict.lists()) - for key, value in d.items(): - if len(value) == 1: - d[key] = value[0] - return d - else: - return multidict.to_dict() + """ Convert a MultiDict containing form data into a regular dict. If the + config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the + same key get entered as a list. If it is False, the first entry is picked. + """ + if config.AUTO_COLLAPSE_MULTI_KEYS: + d = dict(multidict.lists()) + for key, value in d.items(): + if len(value) == 1: + d[key] = value[0] + return d + else: + return multidict.to_dict() class RateLimit(object): - """ Implements the Rate-Limiting logic using Redis as a backend. + """ Implements the Rate-Limiting logic using Redis as a backend. - :param key_prefix: the key used to uniquely identify a client. - :param limit: requests limit, per period. - :param period: limit validity period - :param send_x_headers: True if response headers are supposed to include - special 'X-RateLimit' headers + :param key_prefix: the key used to uniquely identify a client. + :param limit: requests limit, per period. + :param period: limit validity period + :param send_x_headers: True if response headers are supposed to include + special 'X-RateLimit' headers - .. versionadded:: 0.0.7 - """ + .. versionadded:: 0.0.7 + """ + # Maybe has something complicated problems. - # Maybe has something complicated problems. + def __init__(self, key, limit, period, send_x_headers=True): + self.reset = int(time.time()) + period + self.key = key + self.limit = limit + self.period = period + self.send_x_headers = send_x_headers + p = app.redis.pipeline() + p.incr(self.key) + p.expireat(self.key, self.reset) + self.current = p.execute()[0] - def __init__(self, key, limit, period, send_x_headers=True): - self.reset = int(time.time()) + period - self.key = key - self.limit = limit - self.period = period - self.send_x_headers = send_x_headers - p = app.redis.pipeline() - p.incr(self.key) - p.expireat(self.key, self.reset) - self.current = p.execute()[0] - - remaining = property(lambda x: x.limit - x.current) - over_limit = property(lambda x: x.current > x.limit) + remaining = property(lambda x: x.limit - x.current) + over_limit = property(lambda x: x.current > x.limit) def get_rate_limit(): - """ If available, returns a RateLimit instance which is valid for the - current request-response. + """ If available, returns a RateLimit instance which is valid for the + current request-response. - .. versionadded:: 0.0.7 - """ - return getattr(g, '_rate_limit', None) + .. versionadded:: 0.0.7 + """ + return getattr(g, '_rate_limit', None) def ratelimit(): - """ Enables support for Rate-Limits on API methods - The key is constructed by default from the remote address or the - authorization.username if authentication is being used. On - a authentication-only API, this will impose a ratelimit even on - non-authenticated users, reducing exposure to DDoS attacks. - - Before the function is executed it increments the rate limit with the help - of the RateLimit class and stores an instance on g as g._rate_limit. Also - if the client is indeed over limit, we return a 429, see - http://tools.ietf.org/html/draft-nottingham-http-new-status-04#section-4 - - .. versionadded:: 0.0.7 - """ - - def decorator(f): - @wraps(f) - def rate_limited(*args, **kwargs): - method_limit = app.config.get('RATE_LIMIT_' + request.method) - if method_limit and app.redis: - limit = method_limit[0] - period = method_limit[1] - # If authorization is being used the key is 'username'. - # Else, fallback to client IP. - key = 'rate-limit/%s' % (request.authorization.username - if request.authorization else - request.remote_addr) - rlimit = RateLimit(key, limit, period, True) - if rlimit.over_limit: - return Response('Rate limit exceeded', 429) - # store the rate limit for further processing by - # send_response - g._rate_limit = rlimit - else: - g._rate_limit = None - return f(*args, **kwargs) - - return rate_limited - - return decorator + """ Enables support for Rate-Limits on API methods + The key is constructed by default from the remote address or the + authorization.username if authentication is being used. On + a authentication-only API, this will impose a ratelimit even on + non-authenticated users, reducing exposure to DDoS attacks. + + Before the function is executed it increments the rate limit with the help + of the RateLimit class and stores an instance on g as g._rate_limit. Also + if the client is indeed over limit, we return a 429, see + http://tools.ietf.org/html/draft-nottingham-http-new-status-04#section-4 + + .. versionadded:: 0.0.7 + """ + def decorator(f): + @wraps(f) + def rate_limited(*args, **kwargs): + method_limit = app.config.get('RATE_LIMIT_' + request.method) + if method_limit and app.redis: + limit = method_limit[0] + period = method_limit[1] + # If authorization is being used the key is 'username'. + # Else, fallback to client IP. + key = 'rate-limit/%s' % (request.authorization.username + if request.authorization else + request.remote_addr) + rlimit = RateLimit(key, limit, period, True) + if rlimit.over_limit: + return Response('Rate limit exceeded', 429) + # store the rate limit for further processing by + # send_response + g._rate_limit = rlimit + else: + g._rate_limit = None + return f(*args, **kwargs) + return rate_limited + return decorator def last_updated(document): - """ Fixes document's LAST_UPDATED field value. Flask-PyMongo returns - timezone-aware values while stdlib datetime values are timezone-naive. - Comparisons between the two would fail. + """ Fixes document's LAST_UPDATED field value. Flask-PyMongo returns + timezone-aware values while stdlib datetime values are timezone-naive. + Comparisons between the two would fail. - If LAST_UPDATE is missing we assume that it has been created outside of the - API context and inject a default value, to allow for proper computing of - Last-Modified header tag. By design all documents return a LAST_UPDATED - (and we don't want to break existing clients). + If LAST_UPDATE is missing we assume that it has been created outside of the + API context and inject a default value, to allow for proper computing of + Last-Modified header tag. By design all documents return a LAST_UPDATED + (and we don't want to break existing clients). - :param document: the document to be processed. + :param document: the document to be processed. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - if config.LAST_UPDATED in document: - return document[config.LAST_UPDATED].replace(tzinfo=None) - else: - return epoch() + .. versionadded:: 0.0.5 + """ + if config.LAST_UPDATED in document: + return document[config.LAST_UPDATED].replace(tzinfo=None) + else: + return epoch() def date_created(document): - """ If DATE_CREATED is missing we assume that it has been created outside - of the API context and inject a default value. By design all documents - return a DATE_CREATED (and we dont' want to break existing clients). + """ If DATE_CREATED is missing we assume that it has been created outside + of the API context and inject a default value. By design all documents + return a DATE_CREATED (and we dont' want to break existing clients). - :param document: the document to be processed. + :param document: the document to be processed. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - return document[config.DATE_CREATED] if config.DATE_CREATED in document \ - else epoch() + .. versionadded:: 0.0.5 + """ + return document[config.DATE_CREATED] if config.DATE_CREATED in document \ + else epoch() def epoch(): - """ A datetime.min alternative which won't crash on us. + """ A datetime.min alternative which won't crash on us. - .. versionchanged:: 0.1.0 - Moved to common.py and renamed as public, so it can also be used by edit - methods (via get_document()). + .. versionchanged:: 0.1.0 + Moved to common.py and renamed as public, so it can also be used by edit + methods (via get_document()). - .. versionadded:: 0.0.5 - """ - return datetime(1970, 1, 1) + .. versionadded:: 0.0.5 + """ + return datetime(1970, 1, 1) def serialize(document, resource=None, schema=None, fields=None): - """ Recursively handles field values that require data-aware serialization. - Relies on the app.data.serializers dictionary. - - .. versionchanged:: 0.7 - Add support for normalizing anyof-like rules inside lists. See #876. - - .. versionchanged:: 0.6 - Add support for normalizing dotted fields. - - .. versionchanged:: 0.5.4 - Fix serialization of lists of lists. See # 614. - - .. versionchanged:: 0.5.3 - Don't block on custom serialization errors so the whole document can - be processed. See #568. - - .. versionchanged:: 0.5.2 - Fix serialization of keyschemas with objectids. See #525. - - .. versionchanged:: 0.3 - Fix serialization of sub-documents. See #244. - - .. versionadded:: 0.1.1 - """ - - normalize_dotted_fields(document) - - if app.data.serializers: - if resource: - schema = config.DOMAIN[resource]['schema'] - if not fields: - fields = document.keys() - for field in fields: - if document[field] is None: - continue - if field in schema: - field_schema = schema[field] - field_type = field_schema.get('type') - if field_type is None: - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - schema = {field: optschema} - serialize(document, schema=schema) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - schema = {field: {'type': opttype}} - serialize(document, schema=schema) - if config.AUTO_CREATE_LISTS and field_type == 'list': - # Convert single values to lists - if not isinstance(document[field], list): - document[field] = [document[field]] - if 'schema' in field_schema: - field_schema = field_schema['schema'] - if 'dict' in (field_type, field_schema.get('type')): - # either a dict or a list of dicts - embedded = [document[field]] if field_type == 'dict' \ - else document[field] - for subdocument in embedded: - if type(subdocument) is not dict: - # value is not a dict - continue serialization - # error will be reported by validation if - # appropriate - continue - elif 'schema' in field_schema: - serialize(subdocument, - schema=field_schema['schema']) - else: - serialize(subdocument, schema=field_schema) - elif field_schema.get('type') == 'list': - # a list of lists - sublist_schema = field_schema.get('schema') - item_type = sublist_schema.get('type') - for sublist in document[field]: - for i, v in enumerate(sublist): - if item_type == 'dict': - serialize(sublist[i], - schema=sublist_schema['schema']) - elif item_type in app.data.serializers: - sublist[i] = serialize_value(item_type, v) - elif field_schema.get('type') is None: - # a list of items determined by *of rules - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - schema = {field: { - 'type': field_type, - 'schema': optschema}} - serialize(document, schema=schema) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - schema = {field: { - 'type': field_type, - 'schema': {'type': opttype}}} - serialize(document, schema=schema) - else: - # a list of one type, arbitrary length - field_type = field_schema.get('type') - if field_type in app.data.serializers: - for i, v in enumerate(document[field]): - document[field][i] = \ - serialize_value(field_type, v) - elif 'items' in field_schema: - # a list of multiple types, fixed length - for i, (s, v) in enumerate(zip(field_schema['items'], - document[field])): - field_type = s.get('type') - if field_type in app.data.serializers: - document[field][i] = \ - serialize_value(field_type, document[field][i]) - elif 'valueschema' in field_schema: - # a valueschema - field_type = field_schema['valueschema']['type'] - if field_type == 'objectid': - target = document[field] - for field in target: - target[field] = \ - serialize_value(field_type, target[field]) - elif field_type == 'dict': - for subdocument in document[field].values(): - serialize( - subdocument, - schema=field_schema['valueschema']['schema']) - elif field_type in app.data.serializers: - # a simple field - document[field] = \ - serialize_value(field_type, document[field]) - - return document + """ Recursively handles field values that require data-aware serialization. + Relies on the app.data.serializers dictionary. + + .. versionchanged:: 0.7 + Add support for normalizing anyof-like rules inside lists. See #876. + + .. versionchanged:: 0.6 + Add support for normalizing dotted fields. + + .. versionchanged:: 0.5.4 + Fix serialization of lists of lists. See # 614. + + .. versionchanged:: 0.5.3 + Don't block on custom serialization errors so the whole document can + be processed. See #568. + + .. versionchanged:: 0.5.2 + Fix serialization of keyschemas with objectids. See #525. + + .. versionchanged:: 0.3 + Fix serialization of sub-documents. See #244. + + .. versionadded:: 0.1.1 + """ + + normalize_dotted_fields(document) + + if app.data.serializers: + if resource: + schema = config.DOMAIN[resource]['schema'] + if not fields: + fields = document.keys() + for field in fields: + if document[field] is None: + continue + if field in schema: + field_schema = schema[field] + field_type = field_schema.get('type') + if field_type is None: + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + schema = {field: optschema} + serialize(document, schema=schema) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + schema = {field: {'type': opttype}} + serialize(document, schema=schema) + if config.AUTO_CREATE_LISTS and field_type == 'list': + # Convert single values to lists + if not isinstance(document[field], list): + document[field] = [document[field]] + if 'schema' in field_schema: + field_schema = field_schema['schema'] + if 'dict' in (field_type, field_schema.get('type')): + # either a dict or a list of dicts + embedded = [document[field]] if field_type == 'dict' \ + else document[field] + for subdocument in embedded: + if type(subdocument) is not dict: + # value is not a dict - continue serialization + # error will be reported by validation if + # appropriate + continue + elif 'schema' in field_schema: + serialize(subdocument, + schema=field_schema['schema']) + else: + serialize(subdocument, schema=field_schema) + elif field_schema.get('type') == 'list': + # a list of lists + sublist_schema = field_schema.get('schema') + item_type = sublist_schema.get('type') + for sublist in document[field]: + for i, v in enumerate(sublist): + if item_type == 'dict': + serialize(sublist[i], + schema=sublist_schema['schema']) + elif item_type in app.data.serializers: + sublist[i] = serialize_value(item_type, v) + elif field_schema.get('type') is None: + # a list of items determined by *of rules + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + schema = {field: { + 'type': field_type, + 'schema': optschema}} + serialize(document, schema=schema) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + schema = {field: { + 'type': field_type, + 'schema': {'type': opttype}}} + serialize(document, schema=schema) + else: + # a list of one type, arbitrary length + field_type = field_schema.get('type') + if field_type in app.data.serializers: + for i, v in enumerate(document[field]): + document[field][i] = \ + serialize_value(field_type, v) + elif 'items' in field_schema: + # a list of multiple types, fixed length + for i, (s, v) in enumerate(zip(field_schema['items'], + document[field])): + field_type = s.get('type') + if field_type in app.data.serializers: + document[field][i] = \ + serialize_value(field_type, document[field][i]) + elif 'valueschema' in field_schema: + # a valueschema + field_type = field_schema['valueschema']['type'] + if field_type == 'objectid': + target = document[field] + for field in target: + target[field] = \ + serialize_value(field_type, target[field]) + elif field_type == 'dict': + for subdocument in document[field].values(): + serialize( + subdocument, + schema=field_schema['valueschema']['schema']) + elif field_type in app.data.serializers: + # a simple field + document[field] = \ + serialize_value(field_type, document[field]) + + return document def serialize_value(field_type, value): - """Serialize value of a given type. Relies on the app.data.serializers - dictionary. - """ - try: - return app.data.serializers[field_type](value) - except (KeyError, ValueError, TypeError, InvalidId): - # value can't be cast or no serializer defined, return as is and - # validation will later report back the issue. - return value + """Serialize value of a given type. Relies on the app.data.serializers + dictionary. + """ + try: + return app.data.serializers[field_type](value) + except (KeyError, ValueError, TypeError, InvalidId): + # value can't be cast or no serializer defined, return as is and + # validation will later report back the issue. + return value def normalize_dotted_fields(document): - """ Normalizes eventual dotted fields so validation can be performed - seamlessly. For example this document: + """ Normalizes eventual dotted fields so validation can be performed + seamlessly. For example this document: - {"location.city": "a nested city"} + {"location.city": "a nested city"} - would be normalized to: + would be normalized to: - {"location": {"city": "a nested city"}} + {"location": {"city": "a nested city"}} - Being recursive, normalizing of sub-documents is also supported. For - example: + Being recursive, normalizing of sub-documents is also supported. For + example: - {"location": {"city": "a city", "sub.address": "a subaddress"}} + {"location": {"city": "a city", "sub.address": "a subaddress"}} - would be normalized to: + would be normalized to: - {"location": {"city": "a city", "sub": {"address": "a subaddress}}} + {"location": {"city": "a city", "sub": {"address": "a subaddress}}} - .. versionchanged:: 0.7 - Fix normalization of nested inputs (#738). + .. versionchanged:: 0.7 + Fix normalization of nested inputs (#738). - .. versionadded:: 0.6 - """ - if isinstance(document, list): - prev = document - for i in prev: - normalize_dotted_fields(i) - elif isinstance(document, dict): - for field in list(document): - if '.' in field: - parts = field.split('.') - prev = document - for part in parts[:-1]: - if part not in prev: - prev[part] = {} - prev = prev[part] - if isinstance(document[field], (dict, list)): - normalize_dotted_fields(document[field]) - prev[parts[-1]] = document[field] - document.pop(field) - elif isinstance(document[field], (dict, list)): - normalize_dotted_fields(document[field]) + .. versionadded:: 0.6 + """ + if isinstance(document, list): + prev = document + for i in prev: + normalize_dotted_fields(i) + elif isinstance(document, dict): + for field in list(document): + if '.' in field: + parts = field.split('.') + prev = document + for part in parts[:-1]: + if part not in prev: + prev[part] = {} + prev = prev[part] + if isinstance(document[field], (dict, list)): + normalize_dotted_fields(document[field]) + prev[parts[-1]] = document[field] + document.pop(field) + elif isinstance(document[field], (dict, list)): + normalize_dotted_fields(document[field]) def build_response_document( - document, resource, embedded_fields, latest_doc=None): - """ Prepares a document for response including generation of ETag and - metadata fields. - - :param document: the document to embed other documents into. - :param resource: the resource name. - :param embedded_fields: the list of fields we are allowed to embed. - :param document: the latest version of document. - - .. versionchanged:: 0.5 - Only compute ETAG if necessary (#369). - Add version support (#475). - - .. versionadded:: 0.4 - """ - resource_def = config.DOMAIN[resource] - - # need to update the document field since the etag must be computed on the - # same document representation that might have been used in the collection - # 'get' method - document[config.DATE_CREATED] = date_created(document) - document[config.LAST_UPDATED] = last_updated(document) - - # Up to v0.4 etags were not stored with the documents. - if config.IF_MATCH and config.ETAG not in document: - ignore_fields = resource_def['etag_ignore_fields'] - document[config.ETAG] = document_etag(document, - ignore_fields=ignore_fields) - - # hateoas links - if resource_def['hateoas'] and resource_def['id_field'] in document: - version = None - if resource_def['versioning'] is True \ - and request.args.get(config.VERSION_PARAM): - version = document[config.VERSION] - - self_dict = {'self': document_link(resource, - document[resource_def['id_field']], - version)} - if config.LINKS not in document: - document[config.LINKS] = self_dict - elif 'self' not in document[config.LINKS]: - document[config.LINKS].update(self_dict) - - # add version numbers - resolve_document_version(document, resource, 'GET', latest_doc) - - # resolve media - resolve_media_files(document, resource) - - # resolve soft delete - if resource_def['soft_delete'] is True: - if document.get(config.DELETED) is None: - document[config.DELETED] = False - elif document[config.DELETED] is True: - # Soft deleted documents are sent without expansion of embedded - # documents. Return before resolving them. - return - - # resolve embedded documents - resolve_embedded_documents(document, resource, embedded_fields) + document, resource, embedded_fields, latest_doc=None): + """ Prepares a document for response including generation of ETag and + metadata fields. + + :param document: the document to embed other documents into. + :param resource: the resource name. + :param embedded_fields: the list of fields we are allowed to embed. + :param document: the latest version of document. + + .. versionchanged:: 0.5 + Only compute ETAG if necessary (#369). + Add version support (#475). + + .. versionadded:: 0.4 + """ + resource_def = config.DOMAIN[resource] + + # need to update the document field since the etag must be computed on the + # same document representation that might have been used in the collection + # 'get' method + document[config.DATE_CREATED] = date_created(document) + document[config.LAST_UPDATED] = last_updated(document) + + # Up to v0.4 etags were not stored with the documents. + if config.IF_MATCH and config.ETAG not in document: + ignore_fields = resource_def['etag_ignore_fields'] + document[config.ETAG] = document_etag(document, + ignore_fields=ignore_fields) + + # hateoas links + if resource_def['hateoas'] and resource_def['id_field'] in document: + version = None + if resource_def['versioning'] is True \ + and request.args.get(config.VERSION_PARAM): + version = document[config.VERSION] + + self_dict = {'self': document_link(resource, + document[resource_def['id_field']], + version)} + if config.LINKS not in document: + document[config.LINKS] = self_dict + elif 'self' not in document[config.LINKS]: + document[config.LINKS].update(self_dict) + + # add version numbers + resolve_document_version(document, resource, 'GET', latest_doc) + + # resolve media + resolve_media_files(document, resource) + + # resolve soft delete + if resource_def['soft_delete'] is True: + if document.get(config.DELETED) is None: + document[config.DELETED] = False + elif document[config.DELETED] is True: + # Soft deleted documents are sent without expansion of embedded + # documents. Return before resolving them. + return + + # resolve embedded documents + resolve_embedded_documents(document, resource, embedded_fields) def field_definition(resource, chained_fields): - """ Resolves query string to resource with dot notation like - 'people.address.city' and returns corresponding field definition - of the resource - - :param resource: the resource name whose field to be accepted. - :param chained_fields: query string to retrieve field definition - - .. versionadded 0.5 - """ - definition = config.DOMAIN[resource] - subfields = chained_fields.split('.') - - for field in subfields: - if field not in definition.get('schema', {}): - if 'data_relation' in definition: - sub_resource = definition['data_relation']['resource'] - definition = config.DOMAIN[sub_resource] - - if field not in definition['schema']: - return - definition = definition['schema'][field] - field_type = definition.get('type') - if field_type == 'list': - definition = definition['schema'] - elif field_type == 'objectid': - pass - return definition + """ Resolves query string to resource with dot notation like + 'people.address.city' and returns corresponding field definition + of the resource + + :param resource: the resource name whose field to be accepted. + :param chained_fields: query string to retrieve field definition + + .. versionadded 0.5 + """ + definition = config.DOMAIN[resource] + subfields = chained_fields.split('.') + + for field in subfields: + if field not in definition.get('schema', {}): + if 'data_relation' in definition: + sub_resource = definition['data_relation']['resource'] + definition = config.DOMAIN[sub_resource] + + if field not in definition['schema']: + return + definition = definition['schema'][field] + field_type = definition.get('type') + if field_type == 'list': + definition = definition['schema'] + elif field_type == 'objectid': + pass + return definition def resolve_embedded_fields(resource, req): - """ Returns a list of validated embedded fields from the incoming request - or from the resource definition is the request does not specify. - - :param resource: the resource name. - :param req: and instace of :class:`eve.utils.ParsedRequest`. - - .. versionchanged:: 0.5 - Enables subdocuments embedding. #389. - - .. versionadded:: 0.4 - """ - embedded_fields = [] - non_embedded_fields = [] - if req.embedded: - # Parse the embedded clause, we are expecting - # something like: '{"user":1}' - try: - client_embedding = json.loads(req.embedded) - except ValueError: - abort(400, description='Unable to parse `embedded` clause') - - # Build the list of fields where embedding is being requested - try: - embedded_fields = [k for k, v in client_embedding.items() - if v == 1] - non_embedded_fields = [k for k, v in client_embedding.items() - if v == 0] - except AttributeError: - # We got something other than a dict - abort(400, description='Unable to parse `embedded` clause') - - embedded_fields = list( - (set(config.DOMAIN[resource]['embedded_fields']) | - set(embedded_fields)) - set(non_embedded_fields)) - - # For each field, is the field allowed to be embedded? - # Pick out fields that have a `data_relation` where `embeddable=True` - enabled_embedded_fields = [] - for field in sorted(embedded_fields, key=lambda a: a.count('.')): - # Reject bogus field names - field_def = field_definition(resource, field) - if field_def: - if field_def.get('type') == 'list': - field_def = field_def['schema'] - if 'data_relation' in field_def and \ - field_def['data_relation'].get('embeddable'): - # or could raise 400 here - enabled_embedded_fields.append(field) - - return enabled_embedded_fields + """ Returns a list of validated embedded fields from the incoming request + or from the resource definition is the request does not specify. + + :param resource: the resource name. + :param req: and instace of :class:`eve.utils.ParsedRequest`. + + .. versionchanged:: 0.5 + Enables subdocuments embedding. #389. + + .. versionadded:: 0.4 + """ + embedded_fields = [] + non_embedded_fields = [] + if req.embedded: + # Parse the embedded clause, we are expecting + # something like: '{"user":1}' + try: + client_embedding = json.loads(req.embedded) + except ValueError: + abort(400, description='Unable to parse `embedded` clause') + + # Build the list of fields where embedding is being requested + try: + embedded_fields = [k for k, v in client_embedding.items() + if v == 1] + non_embedded_fields = [k for k, v in client_embedding.items() + if v == 0] + except AttributeError: + # We got something other than a dict + abort(400, description='Unable to parse `embedded` clause') + + embedded_fields = list( + (set(config.DOMAIN[resource]['embedded_fields']) | + set(embedded_fields)) - set(non_embedded_fields)) + + # For each field, is the field allowed to be embedded? + # Pick out fields that have a `data_relation` where `embeddable=True` + enabled_embedded_fields = [] + for field in sorted(embedded_fields, key=lambda a: a.count('.')): + # Reject bogus field names + field_def = field_definition(resource, field) + if field_def: + if field_def.get('type') == 'list': + field_def = field_def['schema'] + if 'data_relation' in field_def and \ + field_def['data_relation'].get('embeddable'): + # or could raise 400 here + enabled_embedded_fields.append(field) + + return enabled_embedded_fields def embedded_document(reference, data_relation, field_name): - """ Returns a document to be embedded by reference using data_relation - taking into account document versions - - :param reference: reference to the document to be embedded. - :param data_relation: the relation schema definition. - :param field_name: field name used in abort message only - - .. versionadded:: 0.5 - """ - # Retrieve and serialize the requested document - if 'version' in data_relation and data_relation['version'] is True: - # grab the specific version - embedded_doc = get_data_version_relation_document( - data_relation, reference) - - # grab the latest version - latest_embedded_doc = get_data_version_relation_document( - data_relation, reference, latest=True) - - # make sure we got the documents - if embedded_doc is None or latest_embedded_doc is None: - # your database is not consistent!!! that is bad - # TODO: we should notify the developers with a log. - abort(404, description=debug_error_message( - "Unable to locate embedded documents for '%s'" % - field_name - )) - - build_response_document(embedded_doc, data_relation['resource'], - [], latest_embedded_doc) - else: - # if reference is DBRef take the referenced collection as subresource - subresource = reference.collection if isinstance(reference, DBRef) \ - else data_relation['resource'] - id_field = config.DOMAIN[subresource]['id_field'] - embedded_doc = app.data.find_one(subresource, None, - **{id_field: reference.id - if isinstance(reference, DBRef) - else reference}) - if embedded_doc: - resolve_media_files(embedded_doc, subresource) - - return embedded_doc + """ Returns a document to be embedded by reference using data_relation + taking into account document versions + + :param reference: reference to the document to be embedded. + :param data_relation: the relation schema definition. + :param field_name: field name used in abort message only + + .. versionadded:: 0.5 + """ + # Retrieve and serialize the requested document + if 'version' in data_relation and data_relation['version'] is True: + # grab the specific version + embedded_doc = get_data_version_relation_document( + data_relation, reference) + + # grab the latest version + latest_embedded_doc = get_data_version_relation_document( + data_relation, reference, latest=True) + + # make sure we got the documents + if embedded_doc is None or latest_embedded_doc is None: + # your database is not consistent!!! that is bad + # TODO: we should notify the developers with a log. + abort(404, description=debug_error_message( + "Unable to locate embedded documents for '%s'" % + field_name + )) + + build_response_document(embedded_doc, data_relation['resource'], + [], latest_embedded_doc) + else: + # if reference is DBRef take the referenced collection as subresource + subresource = reference.collection if isinstance(reference, DBRef) \ + else data_relation['resource'] + id_field = config.DOMAIN[subresource]['id_field'] + embedded_doc = app.data.find_one(subresource, None, + **{id_field: reference.id + if isinstance(reference, DBRef) + else reference}) + if embedded_doc: + resolve_media_files(embedded_doc, subresource) + + return embedded_doc def subdocuments(fields_chain, resource, document): - """ Traverses the given document and yields subdocuments which - correspond to the given fields_chain - - :param fields_chain: list of nested field names. - :param resource: the resource name. - :param document: document to be traversed - - .. versionadded:: 0.5 - """ - if len(fields_chain) == 0: - yield document - elif isinstance(document, dict) and fields_chain[0] in document: - subdocument = document[fields_chain[0]] - docs = subdocument if isinstance(subdocument, list) else [subdocument] - try: - resource = field_definition( - resource, fields_chain[0])['data_relation']['resource'] - except KeyError: - resource = resource - - for doc in docs: - for result in subdocuments(fields_chain[1:], resource, doc): - yield result - else: - yield document + """ Traverses the given document and yields subdocuments which + correspond to the given fields_chain + + :param fields_chain: list of nested field names. + :param resource: the resource name. + :param document: document to be traversed + + .. versionadded:: 0.5 + """ + if len(fields_chain) == 0: + yield document + elif isinstance(document, dict) and fields_chain[0] in document: + subdocument = document[fields_chain[0]] + docs = subdocument if isinstance(subdocument, list) else [subdocument] + try: + resource = field_definition( + resource, fields_chain[0])['data_relation']['resource'] + except KeyError: + resource = resource + + for doc in docs: + for result in subdocuments(fields_chain[1:], resource, doc): + yield result + else: + yield document def resolve_embedded_documents(document, resource, embedded_fields): - """ Loops through the documents, adding embedded representations - of any fields that are (1) defined eligible for embedding in the - DOMAIN and (2) requested to be embedded in the current `req`. - - Currently we support embedding of documents by references located - in any subdocuments. For example, query embedded={"user.friends":1} - will return a document with "user" and all his "friends" embedded, - but only if "user" is a subdocument. - - We do not support multiple layers embeddings. - - :param document: the document to embed other documents into. - :param resource: the resource name. - :param embedded_fields: the list of fields we are allowed to embed. - - .. versionchanged:: 0.5 - Support for embedding documents located in subdocuments. - Allocated two functions embedded_document and subdocuments. - - .. versionchanged:: 0.4 - Moved parsing of embedded fields to _resolve_embedded_fields. - Support for document versioning. - - .. versionchanged:: 0.2 - Support for 'embedded_fields'. - - .. versionchanged:: 0.1.1 - 'collection' key has been renamed to 'resource' (data_relation). - - .. versionadded:: 0.1.0 - """ - # NOTE(Gonéri): We resolve the embedded documents at the end. - for field in sorted(embedded_fields, key=lambda a: a.count('.')): - data_relation = field_definition(resource, field)['data_relation'] - getter = lambda ref: embedded_document(ref, data_relation, field) # noqa - fields_chain = field.split('.') - last_field = fields_chain[-1] - for subdocument in subdocuments(fields_chain[:-1], resource, document): - if last_field not in subdocument: - continue - if isinstance(subdocument[last_field], list): - subdocument[last_field] = list(map(getter, - subdocument[last_field])) - else: - subdocument[last_field] = getter(subdocument[last_field]) + """ Loops through the documents, adding embedded representations + of any fields that are (1) defined eligible for embedding in the + DOMAIN and (2) requested to be embedded in the current `req`. + + Currently we support embedding of documents by references located + in any subdocuments. For example, query embedded={"user.friends":1} + will return a document with "user" and all his "friends" embedded, + but only if "user" is a subdocument. + + We do not support multiple layers embeddings. + + :param document: the document to embed other documents into. + :param resource: the resource name. + :param embedded_fields: the list of fields we are allowed to embed. + + .. versionchanged:: 0.5 + Support for embedding documents located in subdocuments. + Allocated two functions embedded_document and subdocuments. + + .. versionchanged:: 0.4 + Moved parsing of embedded fields to _resolve_embedded_fields. + Support for document versioning. + + .. versionchanged:: 0.2 + Support for 'embedded_fields'. + + .. versionchanged:: 0.1.1 + 'collection' key has been renamed to 'resource' (data_relation). + + .. versionadded:: 0.1.0 + """ + # NOTE(Gonéri): We resolve the embedded documents at the end. + for field in sorted(embedded_fields, key=lambda a: a.count('.')): + data_relation = field_definition(resource, field)['data_relation'] + getter = lambda ref: embedded_document(ref, data_relation, field) # noqa + fields_chain = field.split('.') + last_field = fields_chain[-1] + for subdocument in subdocuments(fields_chain[:-1], resource, document): + if last_field not in subdocument: + continue + if isinstance(subdocument[last_field], list): + subdocument[last_field] = list(map(getter, + subdocument[last_field])) + else: + subdocument[last_field] = getter(subdocument[last_field]) def resolve_media_files(document, resource): - """ Embed media files into the response document. + """ Embed media files into the response document. - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. - .. versionadded:: 0.4 - """ - for field in resource_media_fields(document, resource): - if isinstance(document[field], list): - resolved_list = [] - for file_id in document[field]: - resolved_list.append(resolve_one_media(file_id, resource)) - document[field] = resolved_list - else: - document[field] = resolve_one_media(document[field], resource) + .. versionadded:: 0.4 + """ + for field in resource_media_fields(document, resource): + if isinstance(document[field], list): + resolved_list = [] + for file_id in document[field]: + resolved_list.append(resolve_one_media(file_id, resource)) + document[field] = resolved_list + else: + document[field] = resolve_one_media(document[field], resource) def resolve_one_media(file_id, resource): - """ Get response for one media file """ - _file = app.media.get(file_id, resource) - - if _file: - # otherwise we have a valid file and should send extended response - # start with the basic file object - if config.RETURN_MEDIA_AS_BASE64_STRING: - ret_file = base64.encodestring(_file.read()) - elif config.RETURN_MEDIA_AS_URL: - prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ - is not None else app.api_prefix - ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, - file_id) - else: - ret_file = None - - if config.EXTENDED_MEDIA_INFO: - ret = { - 'file': ret_file, - } - - # check if we should return any special fields - for attribute in config.EXTENDED_MEDIA_INFO: - if hasattr(_file, attribute): - # add extended field if found in the file object - ret.update({ - attribute: getattr(_file, attribute) - }) - else: - # tried to select an invalid attribute - abort(500, description=debug_error_message( - 'Invalid extended media attribute requested' - )) - - return ret - else: - return ret_file - else: - return None + """ Get response for one media file """ + _file = app.media.get(file_id, resource) + + if _file: + # otherwise we have a valid file and should send extended response + # start with the basic file object + if config.RETURN_MEDIA_AS_BASE64_STRING: + ret_file = base64.encodestring(_file.read()) + elif config.RETURN_MEDIA_AS_URL: + prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ + is not None else app.api_prefix + ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, + file_id) + else: + ret_file = None + + if config.EXTENDED_MEDIA_INFO: + ret = { + 'file': ret_file, + } + + # check if we should return any special fields + for attribute in config.EXTENDED_MEDIA_INFO: + if hasattr(_file, attribute): + # add extended field if found in the file object + ret.update({ + attribute: getattr(_file, attribute) + }) + else: + # tried to select an invalid attribute + abort(500, description=debug_error_message( + 'Invalid extended media attribute requested' + )) + + return ret + else: + return ret_file + else: + return None def marshal_write_response(document, resource): - """ Limit response document to minimize bandwidth when client supports it. - - :param document: the response document. - :param resource: the resource being consumed by the request. - - .. versionchanged: 0.5 - Avoid exposing 'auth_field' if it is not intended to be public. - - .. versionadded:: 0.4 - """ - - resource_def = app.config['DOMAIN'][resource] - if app.config['BANDWIDTH_SAVER'] is True: - # only return the automatic fields and special extra fields - fields = auto_fields(resource) + resource_def['extra_response_fields'] - document = dict((k, v) for (k, v) in document.items() if k in fields) - else: - # avoid exposing the auth_field if it is not included in the - # resource schema. - auth_field = resource_def.get('auth_field') - if auth_field and auth_field not in resource_def['schema']: - try: - del (document[auth_field]) - except: - # 'auth_field' value has not been set by the auth class. - pass - return document + """ Limit response document to minimize bandwidth when client supports it. + + :param document: the response document. + :param resource: the resource being consumed by the request. + + .. versionchanged: 0.5 + Avoid exposing 'auth_field' if it is not intended to be public. + + .. versionadded:: 0.4 + """ + + resource_def = app.config['DOMAIN'][resource] + if app.config['BANDWIDTH_SAVER'] is True: + # only return the automatic fields and special extra fields + fields = auto_fields(resource) + resource_def['extra_response_fields'] + document = dict((k, v) for (k, v) in document.items() if k in fields) + else: + # avoid exposing the auth_field if it is not included in the + # resource schema. + auth_field = resource_def.get('auth_field') + if auth_field and auth_field not in resource_def['schema']: + try: + del(document[auth_field]) + except: + # 'auth_field' value has not been set by the auth class. + pass + return document def store_media_files(document, resource, original=None): - """ Store any media file in the underlying media store and update the - document with unique ids of stored files. - - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. - :param original: original document being replaced or edited. - - .. versionchanged:: 0.4 - Renamed to store_media_files to deconflict with new resolve_media_files. - - .. versionadded:: 0.3 - """ - # TODO We're storing media files in advance, before the corresponding - # document is also stored. In the rare occurrence that the subsequent - # document update fails we should probably attempt a cleanup on the storage - # system. Easier said than done though. - for field in resource_media_fields(document, resource): - if original and field in original: - # since file replacement is not supported by the media storage - # system, we first need to delete the files being replaced. - if isinstance(original[field], list): - for file_id in original[field]: - app.media.delete(file_id, resource) - else: - app.media.delete(original[field], resource) - - if document[field]: - # store files and update document with file's unique id/filename - # also pass in mimetype for use when retrieving the file - if isinstance(document[field], list): - id_lst = [] - for stor_obj in document[field]: - id_lst.append(app.media.put( - stor_obj, filename=stor_obj.filename, - content_type=stor_obj.mimetype, resource=resource)) - document[field] = id_lst - else: - document[field] = app.media.put( - document[field], filename=document[field].filename, - content_type=document[field].mimetype, resource=resource) + """ Store any media file in the underlying media store and update the + document with unique ids of stored files. + + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. + :param original: original document being replaced or edited. + + .. versionchanged:: 0.4 + Renamed to store_media_files to deconflict with new resolve_media_files. + + .. versionadded:: 0.3 + """ + # TODO We're storing media files in advance, before the corresponding + # document is also stored. In the rare occurrence that the subsequent + # document update fails we should probably attempt a cleanup on the storage + # system. Easier said than done though. + for field in resource_media_fields(document, resource): + if original and field in original: + # since file replacement is not supported by the media storage + # system, we first need to delete the files being replaced. + if isinstance(original[field], list): + for file_id in original[field]: + app.media.delete(file_id, resource) + else: + app.media.delete(original[field], resource) + + if document[field]: + # store files and update document with file's unique id/filename + # also pass in mimetype for use when retrieving the file + if isinstance(document[field], list): + id_lst = [] + for stor_obj in document[field]: + id_lst.append(app.media.put( + stor_obj, filename=stor_obj.filename, + content_type=stor_obj.mimetype, resource=resource)) + document[field] = id_lst + else: + document[field] = app.media.put( + document[field], filename=document[field].filename, + content_type=document[field].mimetype, resource=resource) def resource_media_fields(document, resource): - """ Returns a list of media fields defined in the resource schema. + """ Returns a list of media fields defined in the resource schema. - :param document: the document eventually containing the media files. - :param resource: the resource being consumed by the request. + :param document: the document eventually containing the media files. + :param resource: the resource being consumed by the request. - .. versionadded:: 0.3 - """ - media_fields = app.config['DOMAIN'][resource]['_media'] - return [field for field in media_fields if field in document] + .. versionadded:: 0.3 + """ + media_fields = app.config['DOMAIN'][resource]['_media'] + return [field for field in media_fields if field in document] def resolve_sub_resource_path(document, resource): - if not request.view_args: - return + if not request.view_args: + return - resource_def = config.DOMAIN[resource] - schema = resource_def['schema'] - fields = [] - for field, value in request.view_args.items(): - if field in schema and field != resource_def['id_field']: - fields.append(field) - document[field] = value + resource_def = config.DOMAIN[resource] + schema = resource_def['schema'] + fields = [] + for field, value in request.view_args.items(): + if field in schema and field != resource_def['id_field']: + fields.append(field) + document[field] = value - if fields: - serialize(document, resource, fields=fields) + if fields: + serialize(document, resource, fields=fields) def resolve_user_restricted_access(document, resource): - """ Adds user restricted access metadata to the document if applicable. + """ Adds user restricted access metadata to the document if applicable. - :param document: the document being posted or replaced - :param resource: the resource to which the document belongs + :param document: the document being posted or replaced + :param resource: the resource to which the document belongs - .. versionchanged:: 0.5.2 - Make User Restricted Resource Access work with HMAC Auth too. + .. versionchanged:: 0.5.2 + Make User Restricted Resource Access work with HMAC Auth too. - .. versionchanged:: 0.4 - Use new auth.request_auth_value() method. + .. versionchanged:: 0.4 + Use new auth.request_auth_value() method. - .. versionadded:: 0.3 - """ - # if 'user-restricted resource access' is enabled and there's - # an Auth request active, inject the username into the document - resource_def = app.config['DOMAIN'][resource] - auth = resource_def['authentication'] - auth_field = resource_def['auth_field'] - if auth and auth_field: - request_auth_value = auth.get_request_auth_value() - if request_auth_value: - document[auth_field] = request_auth_value + .. versionadded:: 0.3 + """ + # if 'user-restricted resource access' is enabled and there's + # an Auth request active, inject the username into the document + resource_def = app.config['DOMAIN'][resource] + auth = resource_def['authentication'] + auth_field = resource_def['auth_field'] + if auth and auth_field: + request_auth_value = auth.get_request_auth_value() + if request_auth_value: + document[auth_field] = request_auth_value def resolve_document_etag(documents, resource): - """ Adds etags to documents. + """ Adds etags to documents. - .. versionadded:: 0.5 - """ - if config.IF_MATCH: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] + .. versionadded:: 0.5 + """ + if config.IF_MATCH: + ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] - if not isinstance(documents, list): - documents = [documents] + if not isinstance(documents, list): + documents = [documents] - for document in documents: - document[config.ETAG] = \ - document_etag(document, ignore_fields=ignore_fields) + for document in documents: + document[config.ETAG] =\ + document_etag(document, ignore_fields=ignore_fields) def pre_event(f): - """ Enable a Hook pre http request. - - .. versionchanged:: 0.6 - Enable callback hooks for HEAD requests. - - .. versionchanged:: 0.4 - Merge 'sub_resource_lookup' (args[1]) with kwargs, so http methods can - all enjoy the same signature, and data layer find methods can seemingly - process both kind of queries. - - .. versionadded:: 0.2 - """ - - @wraps(f) - def decorated(*args, **kwargs): - method = request.method - if method == 'HEAD': - method = 'GET' - - event_name = 'on_pre_' + method - resource = args[0] if args else None - gh_params = () - rh_params = () - if method in ('GET', 'PATCH', 'DELETE', 'PUT'): - gh_params = (resource, request, kwargs) - rh_params = (request, kwargs) - elif method in ('POST',): - # POST hook does not support the kwargs argument - gh_params = (resource, request) - rh_params = (request,) - - # general hook - getattr(app, event_name)(*gh_params) - if resource: - # resource hook - getattr(app, event_name + '_' + resource)(*rh_params) - - combined_args = kwargs - if len(args) > 1: - combined_args.update(args[1].items()) - r = f(resource, **combined_args) - return r - - return decorated + """ Enable a Hook pre http request. + + .. versionchanged:: 0.6 + Enable callback hooks for HEAD requests. + + .. versionchanged:: 0.4 + Merge 'sub_resource_lookup' (args[1]) with kwargs, so http methods can + all enjoy the same signature, and data layer find methods can seemingly + process both kind of queries. + + .. versionadded:: 0.2 + """ + @wraps(f) + def decorated(*args, **kwargs): + method = request.method + if method == 'HEAD': + method = 'GET' + + event_name = 'on_pre_' + method + resource = args[0] if args else None + gh_params = () + rh_params = () + if method in ('GET', 'PATCH', 'DELETE', 'PUT'): + gh_params = (resource, request, kwargs) + rh_params = (request, kwargs) + elif method in ('POST', ): + # POST hook does not support the kwargs argument + gh_params = (resource, request) + rh_params = (request,) + + # general hook + getattr(app, event_name)(*gh_params) + if resource: + # resource hook + getattr(app, event_name + '_' + resource)(*rh_params) + + combined_args = kwargs + if len(args) > 1: + combined_args.update(args[1].items()) + r = f(resource, **combined_args) + return r + return decorated def document_link(resource, document_id, version=None): - """ Returns a link to a document endpoint. + """ Returns a link to a document endpoint. - :param resource: the resource name. - :param document_id: the document unique identifier. - :param version: the document version. Defaults to None. + :param resource: the resource name. + :param document_id: the document unique identifier. + :param version: the document version. Defaults to None. - .. versionchanged:: 0.5 - Add version support (#475). + .. versionchanged:: 0.5 + Add version support (#475). - .. versionchanged:: 0.4 - Use the regex-neutral resource_link function. + .. versionchanged:: 0.4 + Use the regex-neutral resource_link function. - .. versionchanged:: 0.1.0 - No more trailing slashes in links. + .. versionchanged:: 0.1.0 + No more trailing slashes in links. - .. versionchanged:: 0.0.3 - Now returning a JSON link - """ - version_part = '?version=%s' % version if version else '' - return {'title': '%s' % config.DOMAIN[resource]['item_title'], - 'href': '%s/%s%s' % (resource_link(), document_id, version_part)} + .. versionchanged:: 0.0.3 + Now returning a JSON link + """ + version_part = '?version=%s' % version if version else '' + return {'title': '%s' % config.DOMAIN[resource]['item_title'], + 'href': '%s/%s%s' % (resource_link(), document_id, version_part)} def resource_link(): - """ Returns the current resource path relative to the API entry point. - Mostly going to be used by hateoas functions when building - document/resource links. The resource URL stored in the config settings - might contain regexes and custom variable names, all of which are not - needed in the response payload. + """ Returns the current resource path relative to the API entry point. + Mostly going to be used by hateoas functions when building + document/resource links. The resource URL stored in the config settings + might contain regexes and custom variable names, all of which are not + needed in the response payload. - .. versionchanged:: 0.5 - URL is relative to API root. + .. versionchanged:: 0.5 + URL is relative to API root. - .. versionadded:: 0.4 - """ - path = request.path.strip('/') + .. versionadded:: 0.4 + """ + path = request.path.strip('/') - if '|item' in request.endpoint: - path = path[:path.rfind('/')] + if '|item' in request.endpoint: + path = path[:path.rfind('/')] - def strip_prefix(hit): - return path[len(hit):] if path.startswith(hit) else path + def strip_prefix(hit): + return path[len(hit):] if path.startswith(hit) else path - if config.URL_PREFIX: - path = strip_prefix(config.URL_PREFIX + '/') - if config.API_VERSION: - path = strip_prefix(config.API_VERSION + '/') - return path + if config.URL_PREFIX: + path = strip_prefix(config.URL_PREFIX + '/') + if config.API_VERSION: + path = strip_prefix(config.API_VERSION + '/') + return path def oplog_push(resource, document, op, id=None): - """ Pushes an edit operation to the oplog if included in OPLOG_METHODS. To - save on storage space (at least on MongoDB) field names are shortened: - - 'r' = resource endpoint, - 'o' = operation performed, - 'i' = unique id of the document involved, - 'pi' = client IP, - 'c' = changes - - config.LAST_UPDATED, config.LAST_CREATED and AUTH_FIELD are not being - shortened to allow for standard endpoint behavior (so clients can - query the endpoint with If-Modified-Since queries, and User-Restricted- - Resource-Access will keep working on the oplog endpoint too). - - :param resource: name of the resource involved. - :param document: updates performed with the edit operation. - :param op: operation performed. Can be 'POST', 'PUT', 'PATCH', 'DELETE'. - :param id: unique id of the document. - - .. versionchanged:: 0.7 - Add user information to the audit. Closes #846. - Raise on_oplog_push event. - Add support for 'extra' custom field. - - .. versionchanged:: 0.5.4 - Use a copy of original document in order to avoid altering its state. - See #590. - - .. versionadded:: 0.5 - """ - if not config.OPLOG \ - or op not in config.OPLOG_METHODS \ - or resource not in config.URLS: - return - - resource_def = config.DOMAIN[resource] - - if document is None: - updates = {} - else: - updates = copy(document) - - if not isinstance(updates, list): - updates = [updates] - - entries = [] - for update in updates: - entry = { - 'r': config.URLS[resource], - 'o': op, - 'i': (update[resource_def['id_field']] - if resource_def['id_field'] in update else id), - } - if config.LAST_UPDATED in update: - last_update = update[config.LAST_UPDATED] - else: - last_update = datetime.utcnow().replace(microsecond=0) - entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update - if config.OPLOG_AUDIT: - entry['ip'] = request.remote_addr - - auth = resource_def['authentication'] - entry['u'] = auth.get_user_or_token() if auth else 'n/a' - - if op in config.OPLOG_CHANGE_METHODS: - # these fields are already contained in 'entry'. - del (update[config.LAST_UPDATED]) - # legacy documents (v0.4 or less) could be missing the etag - # field - if config.ETAG in update: - del (update[config.ETAG]) - entry['c'] = update - else: - pass - - resolve_user_restricted_access(entry, config.OPLOG_NAME) - - entries.append(entry) - - if entries: - # notify callbacks - getattr(app, "on_oplog_push")(resource, entries) - # oplog push - app.data.insert(config.OPLOG_NAME, entries) + """ Pushes an edit operation to the oplog if included in OPLOG_METHODS. To + save on storage space (at least on MongoDB) field names are shortened: + + 'r' = resource endpoint, + 'o' = operation performed, + 'i' = unique id of the document involved, + 'pi' = client IP, + 'c' = changes + + config.LAST_UPDATED, config.LAST_CREATED and AUTH_FIELD are not being + shortened to allow for standard endpoint behavior (so clients can + query the endpoint with If-Modified-Since queries, and User-Restricted- + Resource-Access will keep working on the oplog endpoint too). + + :param resource: name of the resource involved. + :param document: updates performed with the edit operation. + :param op: operation performed. Can be 'POST', 'PUT', 'PATCH', 'DELETE'. + :param id: unique id of the document. + + .. versionchanged:: 0.7 + Add user information to the audit. Closes #846. + Raise on_oplog_push event. + Add support for 'extra' custom field. + + .. versionchanged:: 0.5.4 + Use a copy of original document in order to avoid altering its state. + See #590. + + .. versionadded:: 0.5 + """ + if not config.OPLOG \ + or op not in config.OPLOG_METHODS\ + or resource in config.URLS[resource]: + return + + resource_def = config.DOMAIN[resource] + + if document is None: + updates = {} + else: + updates = copy(document) + + if not isinstance(updates, list): + updates = [updates] + + entries = [] + for update in updates: + entry = { + 'r': config.URLS[resource], + 'o': op, + 'i': (update[resource_def['id_field']] + if resource_def['id_field'] in update else id), + } + if config.LAST_UPDATED in update: + last_update = update[config.LAST_UPDATED] + else: + last_update = datetime.utcnow().replace(microsecond=0) + entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update + if config.OPLOG_AUDIT: + entry['ip'] = request.remote_addr + + auth = resource_def['authentication'] + entry['u'] = auth.get_user_or_token() if auth else 'n/a' + + if op in config.OPLOG_CHANGE_METHODS: + # these fields are already contained in 'entry'. + del(update[config.LAST_UPDATED]) + # legacy documents (v0.4 or less) could be missing the etag + # field + if config.ETAG in update: + del(update[config.ETAG]) + entry['c'] = update + else: + pass + + resolve_user_restricted_access(entry, config.OPLOG_NAME) + + entries.append(entry) + + if entries: + # notify callbacks + getattr(app, "on_oplog_push")(resource, entries) + # oplog push + app.data.insert(config.OPLOG_NAME, entries) From a7be04c9f23a05d5337e621964e7ecce3afe7198 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 24 Apr 2017 15:11:31 +0200 Subject: [PATCH 160/821] Changelog for #1013 --- CHANGES | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 6d04572e6..920993909 100644 --- a/CHANGES +++ b/CHANGES @@ -8,11 +8,13 @@ Development Version 0.7.3 ~~~~~~~~~~~~~ +- Fix: Internal resource, oplog enabled: a ``*_internal`` method defined in + ``OPLOG_METHODS`` triggers keyerror (Einar Huseby). - Dev: use official Alabaster theme instead of custom fork. - Fix: docstrings typos (Martin Fous). -- Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose - the whole document as found in the database, with no explicit validation - schema. Addresses #995. +- Docs: explain that ``ALLOW_UNKNOWN`` can also be used to expose the whole + document as found in the database, with no explicit validation schema. + Addresses #995. - Docs: add Eve-Healthcheck to extensions list (Luis Fernando Gomes). Stable From f234cec877f88772d16cebdbe8581495bb2bdd57 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 24 Apr 2017 15:12:02 +0200 Subject: [PATCH 161/821] Einar Huseby --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 5bfbfbb2f..062659a9c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -37,6 +37,7 @@ Patches and Contributions - Dominik Kellner - Dong Wei Ming - Dougal Matthews +- Einar Huseby - Emmanuel Leblond - Eugene Prikazchikov - Felix Peppert From c9be4e588fdd4e74902550ec4a74108c50658948 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Mar 2017 09:59:33 +0100 Subject: [PATCH 162/821] Announce the Funding Eve inititative. - README - Funding page (new) - Homepage --- README.rst | 17 +++++++++++++---- docs/funding.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 14 ++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 docs/funding.rst diff --git a/README.rst b/README.rst index ca80ad36f..0d1970c41 100644 --- a/README.rst +++ b/README.rst @@ -5,12 +5,20 @@ Eve Eve is an open source Python REST API framework designed for human beings. It allows to effortlessly build and deploy highly customizable, fully featured -RESTful Web Services. +RESTful Web Services. Eve offers native support for MongoDB, and SQL backends +via community extensions. -Eve is powered by Flask, Redis, Cerberus, Events and offers support for both -MongoDB and SQL backends. +Funding +------- +Eve REST framework is a open source, collaboratively funded project. If you run +a business and are using Eve in a revenue-generating product, it would make +business sense to sponsor Eve development: it ensures the project that your +product relies on stays healthy and actively maintained. Individual users are +also welcome to make a recurring pledge or a one time donation if Eve has +helped you in your work or personal projects. -The codebase is thoroughly tested under Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and PyPy. +Every single sign-up makes a significant impact towards making Eve possible. To +learn more, check out our `funding page`_. Eve is Simple ------------- @@ -80,3 +88,4 @@ distributed under the `BSD license `_. .. _`Nicola Iarocci`: http://nicolaiarocci.com +.. _`funding page`: http://python-eve.org/funding diff --git a/docs/funding.rst b/docs/funding.rst new file mode 100644 index 000000000..ea8af10b5 --- /dev/null +++ b/docs/funding.rst @@ -0,0 +1,42 @@ +Funding +======= +We believe that collaboratively funded software can offer outstanding returns +on investment, by encouraging users to collectively share the cost of +development. + +The Eve REST framework continues to be open-source and permissively licensed, +but we firmly believe it is in the commercial best-interest for users of the +project to invest in its ongoing development. + +Signing up as a Backer or Sponsor will: + +- Directly contribute to faster releases, more features, and higher quality software. +- Allow more time to be invested in documentation, issue triage, and community support. +- Safeguard the future development of the Eve REST framework. + +If you run a business and is using Eve in a revenue-generating product, it +would make business sense to sponsor Eve development: it ensures the project +that your product relies on stays healthy and actively maintained. It can also +help your exposure in the Eve community and makes it easier to attract Eve +developers. + +Of course, individual users are also welcome to make a recurring pledge if Eve +has helped you in your work or personal projects. Alternatively, consider +donating as a sign of appreciation - like buying me coffee once in a while :) + +Support Eve development +----------------------- +You can support Eve development by pledging on Patreon or donating on PayPal. + +- `Become a Backer or Sponsor `_ (recurring pledge) +- `Donate via PayPal <#>`_ (one time) + + +Custom Sponsorship and Consulting +--------------------------------- +If you are a business that is building core products using Eve, I am also +open to conversations regarding custom sponsorship / consulting arrangements. +Just `get in touch`_ with me. + + +.. _`get in touch`: mailto:nicola@nicolaiarocci.com diff --git a/docs/index.rst b/docs/index.rst index f71d57773..b49810930 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,6 +52,18 @@ All you need to bring your API online is a database, a configuration file (defaults to ``settings.py``) and a launch script. Overall, you will find that configuring and fine-tuning your API is a very simple process. +Funding Eve +----------- +Eve REST framework is a :doc:`collaboratively funded project `. If you +run a business and are using Eve in a revenue-generating product, it would make +business sense to sponsor Eve development: it ensures the project that your +product relies on stays healthy and actively maintained. Individual users are +also welcome to make either a recurring pledge or a one time donation if Eve +has helped you in your work or personal projects. Every single sign-up makes +a significant impact towards making Eve possible. + +To join the backer ranks, check out `Eve campaign on Patreon`_. + .. _demo: Live demo @@ -81,6 +93,7 @@ link `_. config validation authentication + funding tutorials/index snippets/index extensions @@ -112,3 +125,4 @@ link `_. .. _Cerberus: http://python-cerberus.org .. _events: https://github.com/pyeve/events .. _extensions: http://python-eve.org/extensions +.. _`Eve campaign on Patreon`: https://www.patreon.com/nicolaiarocci From 2a6335ac3a03f214218c18aa087712edf845a68f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 6 Mar 2017 09:28:49 +0100 Subject: [PATCH 163/821] Docs: remove obsolete paragraph from foreword. --- docs/foreword.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index d105fa90c..35cb75a7b 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -41,12 +41,6 @@ number of use cases. I could then release it as an open source project. Well it turned out to be slightly more complex than that but finally here it is, and of course it's called Eve. -It still has a long way to go before it becomes the fully featured open source, -out-of-the-box API solution I envision (see the Roadmap below), but -I feel that at this point the codebase is ready for a public preview. -This will hopefully allow for some constructive feedback and maybe, for some -contributors to join the ranks. - REST, Flask and MongoDB ----------------------- The slides from my EuroPython talk, *Developing RESTful Web APIs with Flask and From 253b10b9a3af46b3ad58caa96c66cabd56dec106 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Mar 2017 10:11:19 +0100 Subject: [PATCH 164/821] Add donate to PayPal link --- docs/funding.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/funding.rst b/docs/funding.rst index ea8af10b5..790666f4b 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -28,8 +28,8 @@ Support Eve development ----------------------- You can support Eve development by pledging on Patreon or donating on PayPal. -- `Become a Backer or Sponsor `_ (recurring pledge) -- `Donate via PayPal <#>`_ (one time) +- `Become a Backer `_ (recurring pledge) +- `Donate via PayPal `_ (one time) Custom Sponsorship and Consulting @@ -38,5 +38,4 @@ If you are a business that is building core products using Eve, I am also open to conversations regarding custom sponsorship / consulting arrangements. Just `get in touch`_ with me. - .. _`get in touch`: mailto:nicola@nicolaiarocci.com From 42b04d0fa62ed72359e30d0976166ad72a862a78 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 28 Apr 2017 09:15:04 +0200 Subject: [PATCH 165/821] Changelog: Eve and Cerberus as collaboratively founded projects --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 920993909..4c7dc7b22 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Development Version 0.7.3 ~~~~~~~~~~~~~ + +Eve and Cerberus are now collaboratively funded projects. See: https://nicolaiarocci.com/eve-and-cerberus-funding-campaign/ + - Fix: Internal resource, oplog enabled: a ``*_internal`` method defined in ``OPLOG_METHODS`` triggers keyerror (Einar Huseby). - Dev: use official Alabaster theme instead of custom fork. From bc753b06655a510b1c2bcc38e7939460aa690537 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 3 May 2017 09:17:39 +0200 Subject: [PATCH 166/821] v0.7.3 release date --- CHANGES | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 4c7dc7b22..937898cbd 100644 --- a/CHANGES +++ b/CHANGES @@ -6,11 +6,16 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Stable +------ + Version 0.7.3 ~~~~~~~~~~~~~ -Eve and Cerberus are now collaboratively funded projects. See: https://nicolaiarocci.com/eve-and-cerberus-funding-campaign/ +Released on 3 May, 2017 +- Eve and Cerberus are now collaboratively funded projects, see: + https://nicolaiarocci.com/eve-and-cerberus-funding-campaign/ - Fix: Internal resource, oplog enabled: a ``*_internal`` method defined in ``OPLOG_METHODS`` triggers keyerror (Einar Huseby). - Dev: use official Alabaster theme instead of custom fork. @@ -20,9 +25,6 @@ Eve and Cerberus are now collaboratively funded projects. See: https://nicolaiar Addresses #995. - Docs: add Eve-Healthcheck to extensions list (Luis Fernando Gomes). -Stable ------- - Version 0.7.2 ~~~~~~~~~~~~~ From 71e38e4f7caf6c76e210e685ca2d910bfac6d907 Mon Sep 17 00:00:00 2001 From: Martin Fous Date: Mon, 27 Mar 2017 00:08:35 +0200 Subject: [PATCH 167/821] Improve GeoJSON validation - New config variable to allow custom fields in GeoJSON (Issue #769) - Validation if coordinates contain at least two values - Support for Feature and FeatureCollection structures --- eve/__init__.py | 2 ++ eve/default_settings.py | 4 +++ eve/io/mongo/geo.py | 31 +++++++++++++++++-- eve/io/mongo/validation.py | 25 ++++++++++++++- eve/tests/io/mongo.py | 62 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 767f3cfeb..a78ae6f31 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -53,6 +53,8 @@ CACHE_CONTROL = 'max-age=10,must-revalidate' # TODO confirm this value CACHE_EXPIRES = 10 +ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False + RESOURCE_METHODS = ['GET'] ITEM_METHODS = ['GET'] ITEM_LOOKUP = True diff --git a/eve/default_settings.py b/eve/default_settings.py index 6da59d429..5f633e264 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -234,6 +234,10 @@ # don't allow unknown key/value pairs for POST/PATCH payloads. ALLOW_UNKNOWN = False +# GeoJSON specs allows any number of key/value pairs +# http://geojson.org/geojson-spec.html#geojson-objects +ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False + # don't ignore unknown schema rules (raise SchemaError) TRANSPARENT_SCHEMA_RULES = False diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index af4d40429..a383b3fb9 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -9,6 +9,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +from eve.utils import config class GeoJSON(dict): @@ -18,11 +19,13 @@ def __init__(self, json): except KeyError: raise TypeError("Not compliant to GeoJSON") self.update(json) - if len(self.keys()) != 2: + if not config.ALLOW_CUSTOM_FIELDS_IN_GEOJSON and \ + len(self.keys()) != 2: raise TypeError("Not compliant to GeoJSON") def _correct_position(self, position): return isinstance(position, list) and \ + len(position) > 1 and \ all(isinstance(pos, int) or isinstance(pos, float) for pos in position) @@ -102,7 +105,31 @@ def __init__(self, json): raise TypeError +class Feature(GeoJSON): + def __init__(self, json): + super(Feature, self).__init__(json) + try: + geometry = self["geometry"] + factory = factories[geometry["type"]] + factory(geometry) + + except (KeyError, TypeError, AttributeError): + raise TypeError("Feature not compliant to GeoJSON") + + +class FeatureCollection(GeoJSON): + def __init__(self, json): + super(FeatureCollection, self).__init__(json) + try: + if not isinstance(self["features"], list): + raise TypeError + for feature in self["features"]: + Feature(feature) + except (KeyError, TypeError, AttributeError): + raise TypeError("FeatureCollection not compliant to GeoJSON") + + factories = dict([(_type.__name__, _type) for _type in [GeometryCollection, Point, MultiPoint, LineString, - MultiLineString, Polygon, MultiPolygon]]) + MultiLineString, Polygon, MultiPolygon]]) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index bb35f06d1..469ef808a 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -21,7 +21,8 @@ from eve.auth import auth_field_and_value from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \ - MultiLineString, MultiPolygon, GeometryCollection + MultiLineString, MultiPolygon, GeometryCollection, Feature, \ + FeatureCollection from eve.utils import config, str_type from eve.versioning import get_data_version_relation_document @@ -443,6 +444,28 @@ def _validate_type_geometrycollection(self, field, value): except TypeError: self._error(field, "GeometryCollection not correct" % value) + def _validate_type_feature(self, field, value): + """ Enables validation for `feature`data type + + :param field: field name. + :param value: field nvalue + """ + try: + Feature(value) + except TypeError: + self._error(field, "Feature not correct" % value) + + def _validate_type_featurecollection(self, field, value): + """ Enables validation for `featurecollection`data type + + :param field: field name. + :param value: field nvalue + """ + try: + FeatureCollection(value) + except TypeError: + self._error(field, "FeatureCollection not correct" % value) + def _error(self, field, _error): """ Change the default behaviour so that, if VALIDATION_ERROR_AS_LIST is enabled, single validation errors are returned as a list. See #536. diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index c5e17cead..378322f1f 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -180,6 +180,14 @@ def test_point_fail(self): self.assertTrue('location' in v.errors) self.assertTrue('Point' in v.errors['location']) + def test_point_coordinates_fail(self): + schema = {'location': {'type': 'point'}} + doc = {'location': {'type': "Point", 'coordinates': [123.0]}} + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('location' in v.errors) + self.assertTrue('Point' in v.errors['location']) + def test_point_integer_success(self): schema = {'location': {'type': 'point'}} doc = {'location': {'type': "Point", 'coordinates': [10, 123.0]}} @@ -290,6 +298,60 @@ def test_geometrycollection_fail(self): self.assertTrue('locations' in v.errors) self.assertTrue('GeometryCollection' in v.errors['locations']) + def test_feature_success(self): + schema = {'locations': {'type': 'feature'}} + doc = {"locations": {"type": "Feature", + "geometry": {"type": "Polygon", + "coordinates": [[[100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0]]]} + } + } + v = Validator(schema) + self.assertTrue(v.validate(doc)) + + def test_feature_fail(self): + schema = {'locations': {'type': 'feature'}} + doc = {"locations": {"type": "Feature", + "geometries": [{"type": "Polygon", + "coordinates": [[[100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 0.0]]]}] + } + } + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('locations' in v.errors) + self.assertTrue('Feature' in v.errors['locations']) + + def test_featurecollection_success(self): + schema = {'locations': {'type': 'featurecollection'}} + doc = {"locations": {"type": "FeatureCollection", + "features": [ + {"type": "Feature", + "geometry": {"type": "Point", + "coordinates": [102.0, 0.5]} + }] + } + } + v = Validator(schema) + self.assertTrue(v.validate(doc)) + + def test_featurecollection_fail(self): + schema = {'locations': {'type': 'featurecollection'}} + doc = {"locations": {"type": "FeatureCollection", + "geometry": {"type": "Point", + "coordinates": [100.0, 0.0]} + } + } + v = Validator(schema) + self.assertFalse(v.validate(doc)) + self.assertTrue('locations' in v.errors) + self.assertTrue('FeatureCollection' in v.errors['locations']) + def test_dependencies_with_defaults(self): schema = { 'test_field': {'dependencies': 'foo'}, From 4a25595c8ffe16b6a1c977e216bca34bcd5c148f Mon Sep 17 00:00:00 2001 From: Martin Fous Date: Mon, 3 Apr 2017 21:36:09 +0200 Subject: [PATCH 168/821] Update GeoJSON documentation --- docs/features.rst | 11 +++++++++-- src/al | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) create mode 160000 src/al diff --git a/docs/features.rst b/docs/features.rst index b0c93d0a3..99302d2ad 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1727,8 +1727,15 @@ encoded in GeoJSON_ format. All GeoJSON objects supported by MongoDB_ are availa - ``MultiPolygon`` - ``GeometryCollection`` -These are implemented as native Eve data types (see :ref:`schema`) so they are -are subject to proper validation. +Eve supports also GeoJSON object Feature and FeatureCollection that are not +explicitely mentioned in MongoDB_ documentation. All these objects are +implemented as native Eve data types (see :ref:`schema`) so they are +are subject to the proper validation. + +GeoJSON specification allows object to contain any number of members (name/value +pairs). Eve validation was implemented to be more strict, allowing only two +members. This restriction can be disabled by setting config variable +ALLOW_CUSTOM_FIELDS_IN_GEOJSON to True. In the example below we are extending the `people` endpoint by adding a ``location`` field is of type Point_. diff --git a/src/al b/src/al new file mode 160000 index 000000000..15d190f29 --- /dev/null +++ b/src/al @@ -0,0 +1 @@ +Subproject commit 15d190f29f86141aab202843f2bf3edfde71e56c From 438d182053680de2a7d5132488d2e22644fa8097 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 4 Apr 2017 10:19:09 +0200 Subject: [PATCH 169/821] Reformat GeoJSON section in docs --- docs/features.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 99302d2ad..efdc78134 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1727,15 +1727,8 @@ encoded in GeoJSON_ format. All GeoJSON objects supported by MongoDB_ are availa - ``MultiPolygon`` - ``GeometryCollection`` -Eve supports also GeoJSON object Feature and FeatureCollection that are not -explicitely mentioned in MongoDB_ documentation. All these objects are -implemented as native Eve data types (see :ref:`schema`) so they are -are subject to the proper validation. - -GeoJSON specification allows object to contain any number of members (name/value -pairs). Eve validation was implemented to be more strict, allowing only two -members. This restriction can be disabled by setting config variable -ALLOW_CUSTOM_FIELDS_IN_GEOJSON to True. +All these objects are implemented as native Eve data types (see :ref:`schema`) +so they are are subject to the proper validation. In the example below we are extending the `people` endpoint by adding a ``location`` field is of type Point_. @@ -1757,6 +1750,13 @@ Storing a contact along with its location is pretty straightforward: $ curl -d '[{"firstname": "barack", "lastname": "obama", "location": {"type":"Point","coordinates":[100.0,10.0]}}]' -H 'Content-Type: application/json' http://127.0.0.1:5000/people HTTP/1.1 201 OK +Eve also supports GeoJSON ``Feature`` and ``FeatureCollection`` objects, which +are not explicitely mentioned in MongoDB_ documentation. GeoJSON specification +allows object to contain any number of members (name/value pairs). Eve +validation was implemented to be more strict, allowing only two members. This +restriction can be disabled by setting ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` to +``True``. + Querying GeoJSON Data ~~~~~~~~~~~~~~~~~~~~~ As a general rule all MongoDB `geospatial query operators`_ and their associated From cc0eab3a1e16825c11f086ab2ca56e04cee54573 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 4 Apr 2017 10:35:16 +0200 Subject: [PATCH 170/821] Changelog for #1004 --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 937898cbd..3110c5f31 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Version 0.8 +~~~~~~~~~~~ +- New: ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` allows custom fields in GeoJSON + (Martin Fous). +- New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. + Closes #769 (Martin Fous). + Stable ------ From 15646dfdfe344a61e9b90c720fc2a85de2e6edc3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Apr 2017 17:39:34 +0200 Subject: [PATCH 171/821] Remove unwanted/uneeded leftover folder (?) --- src/al | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/al diff --git a/src/al b/src/al deleted file mode 160000 index 15d190f29..000000000 --- a/src/al +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 15d190f29f86141aab202843f2bf3edfde71e56c From 2cf7d8a64600a688e5560ab14e23563441d4e434 Mon Sep 17 00:00:00 2001 From: Artem Kolesnikov Date: Mon, 24 Apr 2017 11:32:21 +1000 Subject: [PATCH 172/821] Drop Flask-PyMongo dependency (fixes #855) --- AUTHORS | 1 + CHANGES | 11 ++++ docs/config.rst | 37 ++++------- eve/default_settings.py | 8 +-- eve/io/mongo/flask_pymongo.py | 121 ++++++++++++++++++++++++++++++++++ eve/io/mongo/mongo.py | 2 +- eve/tests/__init__.py | 2 +- eve/tests/io/flask_pymongo.py | 69 +++++++++++++++++++ eve/tests/io/multi_mongo.py | 2 +- requirements.txt | 1 - 10 files changed, 219 insertions(+), 35 deletions(-) create mode 100644 eve/io/mongo/flask_pymongo.py create mode 100644 eve/tests/io/flask_pymongo.py diff --git a/AUTHORS b/AUTHORS index 062659a9c..4ef7a6ce7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Patches and Contributions - Antonio Lourenco - Arnau Orriols - Arthur Burkart +- Artem Kolesnikov - Ashley Roach - Ben Demaree - Bjorn Andersson diff --git a/CHANGES b/CHANGES index 3110c5f31..3c881ad4c 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,17 @@ Version 0.8 (Martin Fous). - New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. Closes #769 (Martin Fous). +- Update: Removed Flask-PyMongo dependency. + - Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` + for naming consistency with PyMongo. + - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, + ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, + ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URL`` + instead. + - Config options ``MONGO_AUTH_MECHANISM`` and + ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. + + Stable ------ diff --git a/docs/config.rst b/docs/config.rst index 6da3575d7..33f27947f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -530,10 +530,6 @@ uppercase. ``MONGO_URI`` A `MongoDB URI`_ which is used in preference of the other configuration variables. -``MONGO_OPTIONS`` MongoDB keyword arguments to passed to - MongoClient class ``__init__``. - Defaults to ``{'connect': True}``. - ``MONGO_HOST`` MongoDB server address. Defaults to ``localhost``. ``MONGO_PORT`` MongoDB port. Defaults to ``27017``. @@ -544,30 +540,19 @@ uppercase. ``MONGO_DBNAME`` MongoDB database name. -``MONGO_AUTHDBNAME`` MongoDB authorization database name. Defaults to ``None``. - -``MONGO_MAX_POOL_SIZE`` The maximum number of idle connections - maintained in the PyMongo connection pool. - Default: PyMongo default. - -``MONGO_SOCKET_TIMEOUT_MS`` How long (in milliseconds) a send or - receive on a socket can take before timing - out. Default: PyMongo default. +``MONGO_OPTIONS`` MongoDB keyword arguments to passed to + MongoClient class ``__init__``. + Defaults to ``{'connect': True, 'tz_aware': True, 'appname': 'flask_app_name'}``. + See `PyMongo mongo_client`_ for reference. -``MONGO_CONNECT_TIMEOUT_MS`` How long (in milliseconds) a connection can - take to be opened before timing out. - Default: PyMongo default. +``MONGO_AUTH_SOURCE`` MongoDB authorization database. Defaults to ``None``. -``MONGO_REPLICA_SET`` The name of a replica set to connect to; - this must match the internal name of the - replica set (as deteremined by the - `isMaster `_ - command). Default: ``None``. +``MONGO_AUTH_MECHANISM`` MongoDB authentication mechanism. + See `PyMongo Authentication Mechanisms`_. + Defaults to ``None``. -``MONGO_READ_PREFERENCE`` Determines how read queries are routed to - the replica set members. Must be one of the - constants defined on PyMongo's ReadPreference_, - or the string names thereof. +``MONGO_AUTH_MECHANISM_PROPERTIES`` Specify MongoDB extra authentication mechanism properties + if required. Defaults to ``None``. ``MONGO_QUERY_BLACKLIST`` A list of Mongo query operators that are not allowed to be used in resource filters @@ -1535,3 +1520,5 @@ read access open to the public. .. _`PyMongo Aggregation Examples`: http://api.mongodb.org/python/current/examples/aggregation.html#aggregation-framework .. _`MongoDB Aggregation Framework`: https://docs.mongodb.org/v3.0/applications/aggregation/ .. _`PyMongo aggregation defaults`: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.aggregate +.. _`PyMongo Authentication Mechanisms`: https://docs.mongodb.com/v3.0/core/authentication-mechanisms/ +.. _`PyMongo mongo_client`: http://api.mongodb.com/python/current/api/pymongo/mongo_client.html diff --git a/eve/default_settings.py b/eve/default_settings.py index 5f633e264..f5049722a 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -247,9 +247,6 @@ RATE_LIMIT_PATCH = None RATE_LIMIT_DELETE = None -# MONGO defaults -MONGO_HOST = 'localhost' -MONGO_PORT = 27017 # disallow Mongo's javascript queries as they might be vulnerable to injection # attacks ('ReDoS' especially), are probably too complex for the average API # end-user and finally can seriously impact overall performance. @@ -258,7 +255,6 @@ # aknowledged writes). This is also the current PyMongo/Mongo default setting. MONGO_WRITE_CONCERN = {'w': 1} MONGO_OPTIONS = { - 'connect': True + 'connect': True, + 'tz_aware': True, } -# Compatibility for flask-pymongo. -MONGO_CONNECT = MONGO_OPTIONS['connect'] diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py new file mode 100644 index 000000000..da1037bf9 --- /dev/null +++ b/eve/io/mongo/flask_pymongo.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +""" + eve.io.mongo.flask_pymongo + ~~~~~~~~~~~~~~~~~~~ + + Flask extension to create Mongo connection and database based on + configuration. + + :copyright: (c) 2017 by Nicola Iarocci. + :license: BSD, see LICENSE for more details. +""" + +from flask import current_app +from pymongo import MongoClient, uri_parser + + +class PyMongo(object): + """ + Creates Mongo connection and database based on Flask configuration. + """ + + def __init__(self, app, config_prefix='MONGO'): + if 'pymongo' not in app.extensions: + app.extensions['pymongo'] = {} + + if config_prefix in app.extensions['pymongo']: + raise Exception('duplicate config_prefix "%s"' % config_prefix) + + self.config_prefix = config_prefix + + def key(suffix): + return '%s_%s' % (config_prefix, suffix) + + def config_to_kwargs(mapping): + """ + Convert config options to kwargs according to provided mapping + information. + """ + kwargs = {} + for option, arg in mapping.items(): + if key(option) in app.config: + kwargs[arg] = app.config[key(option)] + return kwargs + + app.config.setdefault(key('HOST'), 'localhost') + app.config.setdefault(key('PORT'), 27017) + app.config.setdefault(key('DBNAME'), app.name) + app.config.setdefault(key('WRITE_CONCERN'), {'w': 1}) + client_kwargs = { + 'appname': app.name, + 'connect': True, + 'tz_aware': True, + } + if key('OPTIONS') in app.config: + client_kwargs.update(app.config[key('OPTIONS')]) + + if key('WRITE_CONCERN') in app.config: + # w, wtimeout, j and fsync + client_kwargs.update(app.config[key('WRITE_CONCERN')]) + + uri_parser.validate_options(client_kwargs) + + if key('URI') in app.config: + host = app.config[key('URI')] + # raises an exception if uri is invalid + mongo_settings = uri_parser.parse_uri(host) + dbname = mongo_settings.get('database') + if not dbname: + raise ValueError('MongoDB URI does not contain database name') + else: + dbname = app.config[key('DBNAME')] + host = app.config[key('HOST')] + client_kwargs['port'] = app.config[key('PORT')] + + client_kwargs['host'] = host + + if key('DOCUMENT_CLASS') in app.config: + client_kwargs['document_class'] = app.config[key('DOCUMENT_CLASS')] + + cx = MongoClient(**client_kwargs) + db = cx[dbname] + + if key('USERNAME') in app.config: + app.config.setdefault(key('PASSWORD'), None) + username = app.config[key('USERNAME')] + password = app.config[key('PASSWORD')] + auth = (username, password) + if any(auth) and not all(auth): + raise Exception( + 'Must set both USERNAME and PASSWORD or neither') + if any(auth): + auth_mapping = { + 'AUTH_MECHANISM': 'mechanism', + 'AUTH_SOURCE': 'source', + 'AUTH_MECHANISM_PROPERTIES': 'authMechanismProperties', + } + auth_kwargs = config_to_kwargs(auth_mapping) + db.authenticate(username, password, **auth_kwargs) + + app.extensions['pymongo'][config_prefix] = (cx, db) + + @property + def cx(self): + """ + Automatically created :class:`~pymongo.Connection` object corresponding + to the provided configuration parameters. + """ + if self.config_prefix not in current_app.extensions['pymongo']: + raise Exception('flask_pymongo extensions is not initialized') + return current_app.extensions['pymongo'][self.config_prefix][0] + + @property + def db(self): + """ + Automatically created :class:`~pymongo.Database` object + corresponding to the provided configuration parameters. + """ + if self.config_prefix not in current_app.extensions['pymongo']: + raise Exception('flask_pymongo extensions is not initialized') + return current_app.extensions['pymongo'][self.config_prefix][1] diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 3ae38b88c..e196b0b6e 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -19,7 +19,7 @@ from bson.dbref import DBRef from copy import copy from flask import abort, request, g -from flask_pymongo import PyMongo +from .flask_pymongo import PyMongo from pymongo import WriteConcern from werkzeug.exceptions import HTTPException diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 699abc6aa..a170dd2ee 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -7,7 +7,7 @@ import os import simplejson as json from datetime import datetime, timedelta -from flask_pymongo import MongoClient +from pymongo import MongoClient from bson import ObjectId from eve.tests.test_settings import MONGO_PASSWORD, MONGO_USERNAME, \ MONGO_DBNAME, DOMAIN, MONGO_HOST, MONGO_PORT diff --git a/eve/tests/io/flask_pymongo.py b/eve/tests/io/flask_pymongo.py new file mode 100644 index 000000000..2849aac99 --- /dev/null +++ b/eve/tests/io/flask_pymongo.py @@ -0,0 +1,69 @@ +from eve.tests import TestBase +from pymongo import MongoClient +from pymongo.errors import OperationFailure +from eve.tests.test_settings import MONGO1_DBNAME, MONGO1_USERNAME, \ + MONGO1_PASSWORD, MONGO_HOST, MONGO_PORT +from eve.io.mongo.flask_pymongo import PyMongo + + +class TestPyMongo(TestBase): + def setUp(self, url_converters=None): + super(TestPyMongo, self).setUp(url_converters) + self._setupdb() + schema = { + 'title': {'type': 'string'}, + } + settings = { + 'schema': schema, + 'mongo_prefix': 'MONGO1', + } + + self.app.register_resource('works', settings) + + def test_auth_params_provided_in_mongo_url(self): + self.app.config['MONGO1_URL'] = \ + 'mongodb://%s:%s@%s:%s' % (MONGO1_USERNAME, MONGO1_PASSWORD, + MONGO_HOST, MONGO_PORT) + with self.app.app_context(): + db = PyMongo(self.app, 'MONGO1').db + self.assertEquals(0, db.works.count()) + + def test_auth_params_provided_in_config(self): + self.app.config['MONGO1_USERNAME'] = MONGO1_USERNAME + self.app.config['MONGO1_PASSWORD'] = MONGO1_PASSWORD + with self.app.app_context(): + db = PyMongo(self.app, 'MONGO1').db + self.assertEquals(0, db.works.count()) + + def test_invalid_auth_params_provided(self): + # if bad username and/or password is provided in MONGO_URL and mongo + # run w\o --auth pymongo won't raise exception + self.app.config['MONGO1_USERNAME'] = 'bad_username' + self.app.config['MONGO1_PASSWORD'] = 'bad_password' + self.assertRaises(OperationFailure, self._pymongo_instance) + + def test_invalid_port(self): + self.app.config['MONGO1_PORT'] = 'bad_value' + self.assertRaises(TypeError, self._pymongo_instance) + + def test_invalid_options(self): + self.app.config['MONGO1_OPTIONS'] = { + 'connectTimeoutMS': 'bad_value' + } + self.assertRaises(ValueError, self._pymongo_instance) + + def test_valid_port(self): + self.app.config['MONGO1_PORT'] = 27017 + with self.app.app_context(): + db = PyMongo(self.app, 'MONGO1').db + self.assertEquals(0, db.works.count()) + + def _setupdb(self): + self.connection = MongoClient() + self.connection.drop_database(MONGO1_DBNAME) + self.connection[MONGO1_DBNAME].add_user(MONGO1_USERNAME, + MONGO1_PASSWORD) + + def _pymongo_instance(self): + with self.app.app_context(): + PyMongo(self.app, 'MONGO1') diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index 1974e6397..29af22366 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -3,7 +3,7 @@ import json from bson import ObjectId -from flask_pymongo import MongoClient +from pymongo import MongoClient import eve from eve.auth import BasicAuth diff --git a/requirements.txt b/requirements.txt index 8d49a7fd4..7b67198e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ Cerberus==0.9.2 Events==0.2.1 -Flask-PyMongo==0.4.1 Flask==0.12 itsdangerous==0.24 Jinja2==2.9.4 From d28cae918cbb1f120ec6861dcd4b9ee9c79057fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 May 2017 18:01:50 +0200 Subject: [PATCH 173/821] Add breaking changes section to v0.8 changelog --- CHANGES | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index 3c881ad4c..9c4e5bad9 100644 --- a/CHANGES +++ b/CHANGES @@ -12,17 +12,19 @@ Version 0.8 (Martin Fous). - New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. Closes #769 (Martin Fous). -- Update: Removed Flask-PyMongo dependency. - - Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` - for naming consistency with PyMongo. - - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, - ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, - ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URL`` - instead. - - Config options ``MONGO_AUTH_MECHANISM`` and - ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. +Breaking Changes +................ +- Dropped Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). +- Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` for + naming consistency with PyMongo. +- Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, + ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, + ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URL`` + instead. +- Config options ``MONGO_AUTH_MECHANISM`` and + ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. Stable ------ From 2d3152f83cd3c1f3a360f458e8f2b68a4326fb7f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 May 2017 09:08:04 +0200 Subject: [PATCH 174/821] Re-arrange breaking changes list in changelog --- CHANGES | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 9c4e5bad9..7a789c2ce 100644 --- a/CHANGES +++ b/CHANGES @@ -12,19 +12,19 @@ Version 0.8 (Martin Fous). - New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. Closes #769 (Martin Fous). +- Dropped Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). +- Config options ``MONGO_AUTH_MECHANISM`` and + ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. Breaking Changes ................ -- Dropped Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). - Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` for naming consistency with PyMongo. - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URL`` instead. -- Config options ``MONGO_AUTH_MECHANISM`` and - ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. Stable ------ From 228c27c58840b29d6ecc6aaf51ec71d968057d08 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 May 2017 09:09:39 +0200 Subject: [PATCH 175/821] typo --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7a789c2ce..66bed5c5c 100644 --- a/CHANGES +++ b/CHANGES @@ -23,7 +23,7 @@ Breaking Changes naming consistency with PyMongo. - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, - ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URL`` + ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URI`` instead. Stable From 78e82915238bb4c8ed67613d26d6ade7ebd81b12 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 7 May 2017 15:14:53 +0200 Subject: [PATCH 176/821] Bring travis.yml up to speed As of latest travis-ci update (May 4th), the old .travis.yml would now work with Python 2.6. Also, let all branches run ci (previously, only 'master' would be tested) --- .travis.yml | 36 ++++++++++++------------------------ tox.ini | 9 +++++++++ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6b091ee7..ebaaf4b4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,16 @@ +sudo: false language: python -install: pip install tox -matrix: - include: - - python: 2.7 - env: TOX_ENV=py26 - - python: 2.7 - env: TOX_ENV=py27 - - python: 2.7 - env: TOX_ENV=py33 - - python: 2.7 - env: TOX_ENV=py34 - - python: 2.7 - env: TOX_ENV=pypy - - python: 2.7 - env: TOX_ENV=flake8 - - python: 3.5 - env: TOX_ENV=py35 - - python: 3.6-dev - env: TOX_ENV=py36 -script: - - tox -e $TOX_ENV +cache: pip +script: tox +python: + - 2.6 + - 2.7 + - 3.3 + - 3.4 + - 3.5 + - 3.6 + - pypy +install: travis_retry pip install tox-travis services: - mongodb - redis-server @@ -28,6 +19,3 @@ before_script: # See https://github.com/travis-ci/travis-ci/issues/1967#issuecomment-42008605 - sleep 15 - mongo eve_test --eval 'db.addUser("test_user", "test_pw");' -branches: - only: - - master diff --git a/tox.ini b/tox.ini index 96ca9b1a4..ddb22ebae 100644 --- a/tox.ini +++ b/tox.ini @@ -8,3 +8,12 @@ commands=python setup.py test {posargs} deps=flake8 basepython=python2 commands=flake8 --ignore=E731 eve {posargs} + +[tox:travis] +2.6 = py26 +2.7 = py27 +3.3 = py33 +3.4 = py34 +3.5 = py35,flake +3.6 = py36 +pypy = pypy From a6b0c6335fd98408bca639ac28ad210a7eed11ba Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 7 May 2017 15:42:31 +0200 Subject: [PATCH 177/821] Add CI changes to the changelog (forgot) --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 66bed5c5c..58ff772dc 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,10 @@ Version 0.8 - Dropped Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). - Config options ``MONGO_AUTH_MECHANISM`` and ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. +- Dev: after the latest update (May 4th) travis-ci would not run tests on + Python 2.6. +- Dev: all branches are now tested on travis-ci. Previously, only 'master' was + being tested. Breaking Changes ................ From 1d122485762efed5cf07e13d682c90b6ee19a6da Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Thu, 11 May 2017 10:51:59 +0500 Subject: [PATCH 178/821] docs/features.rst: python3 compatible examples --- docs/features.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index efdc78134..9323c4a17 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1108,10 +1108,10 @@ You can subscribe to these events with multiple callback functions. .. code-block:: pycon >>> def pre_get_callback(resource, request, lookup): - ... print 'A GET request on the "%s" endpoint has just been received!' % resource + ... print('A GET request on the "%s" endpoint has just been received!' % resource) >>> def pre_contacts_get_callback(request, lookup): - ... print 'A GET request on the contacts endpoint has just been received!' + ... print('A GET request on the contacts endpoint has just been received!') >>> app = Eve() @@ -1159,10 +1159,10 @@ payload. .. code-block:: pycon >>> def post_get_callback(resource, request, payload): - ... print 'A GET on the "%s" endpoint was just performed!' % resource + ... print('A GET on the "%s" endpoint was just performed!' % resource) >>> def post_contacts_get_callback(request, payload): - ... print 'A get on "contacts" was just performed!' + ... print('A get on "contacts" was just performed!') >>> app = Eve() @@ -1301,16 +1301,16 @@ the items as needed before they are returned to the client. .. code-block:: pycon >>> def before_returning_items(resource_name, response): - ... print 'About to return items from "%s" ' % resource_name + ... print('About to return items from "%s" ' % resource_name) >>> def before_returning_contacts(response): - ... print 'About to return contacts' + ... print('About to return contacts') >>> def before_returning_item(resource_name, response): - ... print 'About to return an item from "%s" ' % resource_name + ... print('About to return an item from "%s" ' % resource_name) >>> def before_returning_contact(response): - ... print 'About to return a contact' + ... print('About to return a contact') >>> app = Eve() >>> app.on_fetched_resource += before_returning_items @@ -1360,10 +1360,10 @@ Example: .. code-block:: pycon >>> def before_insert(resource_name, items): - ... print 'About to store items to "%s" ' % resource + ... print('About to store items to "%s" ' % resource) >>> def after_insert_contacts(items): - ... print 'About to store contacts' + ... print('About to store contacts') >>> app = Eve() >>> app.on_insert += before_insert From ff26c426322240d88ff6c50782d4c0b93305f7bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 15 May 2017 11:42:12 +0200 Subject: [PATCH 179/821] Changelog for #1019 --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 58ff772dc..bb89614e5 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,7 @@ Version 0.8 Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. +- Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). Breaking Changes ................ From b17b83a62525e53643c9a99dc83db5de872dd472 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 15 May 2017 11:43:23 +0200 Subject: [PATCH 180/821] Pahaz Blinov --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 4ef7a6ce7..a0f897753 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,8 +13,8 @@ Patches and Contributions - Andrés Martano - Antonio Lourenco - Arnau Orriols -- Arthur Burkart - Artem Kolesnikov +- Arthur Burkart - Ashley Roach - Ben Demaree - Bjorn Andersson @@ -115,6 +115,7 @@ Patches and Contributions - Olivier Poitrey - Ondrej Slinták - Or Neeman +- Pahaz Blinov - Patrick Decat - Pau Freixes - Paul Doucet From 12ddf78f9515f110d5d6aeda5d5a65e486e86390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 5 Aug 2016 11:01:41 +0200 Subject: [PATCH 181/821] Added unit test for issue 810. The problem seems to be that code called by post_internal() relies on the Flask request object, even though the current request may not be a POST to the given resource. --- eve/methods/common.py | 2 ++ eve/tests/endpoints.py | 21 +++++++++++++++++++++ eve/tests/test_prefix.py | 1 + 3 files changed, 24 insertions(+) diff --git a/eve/methods/common.py b/eve/methods/common.py index 58aa60f68..4a40d9bf4 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1080,6 +1080,8 @@ def resource_link(): """ path = request.path.strip('/') + assert request.routing_exception is None, 'Routing error for %s: %s' % (request.url, request.routing_exception) + if '|item' in request.endpoint: path = path[:path.rfind('/')] diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index bd94effab..74e71d377 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -218,6 +218,27 @@ def test_api_prefix(self): r = self.test_prefix.get('/prefix/contacts/') self.assert200(r.status_code) + r = self.test_prefix.post('/prefix/contacts/', data='{}', + content_type='application/json') + self.assert201(r.status_code) + + def test_api_prefix_post_internal(self): + from eve.methods.post import post_internal + + settings_file = os.path.join(self.this_directory, 'test_prefix.py') + self.app = Eve(settings=settings_file) + self.test_prefix = self.app.test_client() + + # This works fine + with self.app.test_request_context(method='POST', path='/prefix/contacts'): + r, _, _, status_code = post_internal('contacts', {}) + self.assert201(status_code) + + # This fails + with self.app.test_request_context(): + r, _, _, status_code = post_internal('contacts', {}) + self.assert201(status_code) + def test_api_prefix_version(self): settings_file = os.path.join(self.this_directory, 'test_prefix_version.py') diff --git a/eve/tests/test_prefix.py b/eve/tests/test_prefix.py index 75d7b981b..baa0ed1f9 100644 --- a/eve/tests/test_prefix.py +++ b/eve/tests/test_prefix.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +RESOURCE_METHODS = ['GET', 'POST'] URL_PREFIX = 'prefix' DOMAIN = {'contacts': {}} From d3090967c0d68341c8c5a7b3d5d16b8d3f35abb5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 24 May 2017 15:00:03 +0200 Subject: [PATCH 182/821] Fix broken (obsolete) test for #810 --- eve/methods/common.py | 2 -- eve/tests/endpoints.py | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 4a40d9bf4..58aa60f68 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1080,8 +1080,6 @@ def resource_link(): """ path = request.path.strip('/') - assert request.routing_exception is None, 'Routing error for %s: %s' % (request.url, request.routing_exception) - if '|item' in request.endpoint: path = path[:path.rfind('/')] diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 74e71d377..46a8e0c0a 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -223,6 +223,7 @@ def test_api_prefix(self): self.assert201(r.status_code) def test_api_prefix_post_internal(self): + # https://github.com/pyeve/eve/issues/810 from eve.methods.post import post_internal settings_file = os.path.join(self.this_directory, 'test_prefix.py') @@ -230,13 +231,14 @@ def test_api_prefix_post_internal(self): self.test_prefix = self.app.test_client() # This works fine - with self.app.test_request_context(method='POST', path='/prefix/contacts'): - r, _, _, status_code = post_internal('contacts', {}) + with self.app.test_request_context( + method='POST', path='/prefix/contacts'): + _, _, _, status_code, _ = post_internal('contacts', {}) self.assert201(status_code) - # This fails + # This fails unless #810 is fixed with self.app.test_request_context(): - r, _, _, status_code = post_internal('contacts', {}) + _, _, _, status_code, _ = post_internal('contacts', {}) self.assert201(status_code) def test_api_prefix_version(self): From 3c65242ba7206936088c69ba24a5e535328cef38 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 24 May 2017 15:01:49 +0200 Subject: [PATCH 183/821] =?UTF-8?q?Sybren=20A.=20St=C3=BCvel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 062659a9c..4e4b3b0b1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -138,6 +138,7 @@ Patches and Contributions - Stanislav Filin - Stanislav Heller - Stratos Gerakakis +- Sybren A. Stüvel - Taylor Brown - Thomas Sileo - Tim Jacobi From b8092327a46e97b006d1db4bc876279510d71580 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 24 May 2017 15:04:32 +0200 Subject: [PATCH 184/821] Fix: post_internal fails with URL_PREFIX or API_VERSION Closes #810. --- CHANGES | 6 ++++++ eve/auth.py | 2 +- eve/methods/common.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 937898cbd..3baecdcb3 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,12 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Version 0.7.4 +~~~~~~~~~~~~~ + +- Fix: ``post_internal`` fails when using ``URL_PREFIX`` or ``API_VERSION``. + Closes #810. + Stable ------ diff --git a/eve/auth.py b/eve/auth.py index d809cc4b3..2c2590d9d 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -291,7 +291,7 @@ def auth_field_and_value(resource): .. versionadded:: 0.3 """ - if '|resource' in request.endpoint: + if request.endpoint and '|resource' in request.endpoint: # We are on a resource endpoint and need to check against # `public_methods` public_method_list_to_check = 'public_methods' diff --git a/eve/methods/common.py b/eve/methods/common.py index 58aa60f68..d816a5b6c 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1080,7 +1080,7 @@ def resource_link(): """ path = request.path.strip('/') - if '|item' in request.endpoint: + if request.endpoint and '|item' in request.endpoint: path = path[:path.rfind('/')] def strip_prefix(hit): From f37991b46e0bb54a3cb63d163fbbe787b383dda7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 24 May 2017 15:20:09 +0200 Subject: [PATCH 185/821] v0.7.4 release date --- CHANGES | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 3baecdcb3..41a1c0fb9 100644 --- a/CHANGES +++ b/CHANGES @@ -6,15 +6,17 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Stable +------ + Version 0.7.4 ~~~~~~~~~~~~~ +Released on 24 May, 2017 + - Fix: ``post_internal`` fails when using ``URL_PREFIX`` or ``API_VERSION``. Closes #810. -Stable ------- - Version 0.7.3 ~~~~~~~~~~~~~ From 56793318e7857216269e764c0c027a0c4f6d92c0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 24 May 2017 16:01:07 +0200 Subject: [PATCH 186/821] Bump version to 0.7.4 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 767f3cfeb..dbe6722f8 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.3' +__version__ = '0.7.4' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 18d803da6..017646734 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.3', + version='0.7.4', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 2d49d2cbbed1f63e8923394c3440bb224f07c028 Mon Sep 17 00:00:00 2001 From: Dominik Kellner Date: Mon, 26 Sep 2016 15:01:44 +0200 Subject: [PATCH 187/821] Support for Cerberus 1.1 (Based on original patch from dkellner) This is a rather big change. I still decided to do a single commit, as intermediate commits would be in a non-working state anyway. Breaking changes: - `keyschema` was renamed to `valueschema` and `propertyschema` to `keyschema` (following changes in Cerberus). - A PATCH on a document which misses a field having a default value will now result in setting this value, even if the field was not provided in the PATCH's payload. - Error messages for `keyschema` are now returned as dictionary. Before: {'propertyschema_dict': 'propertyschema_dict'} Now: {'keyschema_dict': {'AAA': "value does not match regex '[a-z]+'"}} - Error messages for `type` validations are different now (following changes in Cerberus). - It is no longer valid to have a field with `default` = None and `nullable` = False. (see patch.py:test_patch_nested_document_nullable_missing) In a nutshell, changes to the codebase are as follows: - Add data layer independent subclass of `cerberus.Validator` * Support new signature of `__init__` and `validate` * Use `_config`-aware properties instead of bare member attributes to pass the `resource`, `document_id` and `persisted_document` to make them available to child validators * Add schema-docstrings to all `_validate_*` methods - Adjust Mongo-specific `Validator` subclass * Adjust `_validate_type_*` methods (following changes in Cerberus) * Add schema-docstrings to all `_validate_*` methods - Add custom ErrorHandler to support `VALIDATION_ERROR_AS_LIST` - A few renames: * `ValidationError` -> `DocumentError` * `propertyschema` -> `keyschema` and `keyschema` -> `valueschema` - Adjust tests due to different validation error messages (mostly for `type`) - Remove `transparent_schema_rules` without replacement - Remove `default`-handling, as Cerberus takes care of this now --- CHANGES | 14 ++ docs/config.rst | 8 +- docs/tutorials/custom_idfields.rst | 6 +- docs/validation.rst | 19 +- eve/default_settings.py | 3 - eve/defaults.py | 119 ---------- eve/flaskapp.py | 15 -- eve/io/mongo/validation.py | 339 +++++------------------------ eve/methods/patch.py | 6 +- eve/methods/post.py | 11 +- eve/methods/put.py | 8 +- eve/tests/config.py | 20 -- eve/tests/default_values.py | 243 --------------------- eve/tests/endpoints.py | 6 +- eve/tests/io/media.py | 4 +- eve/tests/io/mongo.py | 38 +--- eve/tests/methods/patch.py | 20 +- eve/tests/methods/post.py | 14 +- eve/tests/test_settings.py | 4 +- eve/tests/versioning.py | 3 +- eve/validation.py | 144 +++++++++++- requirements.txt | 2 +- setup.py | 2 +- 23 files changed, 271 insertions(+), 777 deletions(-) delete mode 100644 eve/defaults.py delete mode 100644 eve/tests/default_values.py diff --git a/CHANGES b/CHANGES index 2f53d6af5..977b21665 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,20 @@ Breaking Changes ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URI`` instead. +- `keyschema` was renamed to `valueschema` and `propertyschema` to + `keyschema` (following changes in Cerberus). +- A PATCH on a document which misses a field having a default value will + now result in setting this value, even if the field was not provided + in the PATCH's payload. +- Error messages for `keyschema` are now returned as dictionary. + Before: {'propertyschema_dict': 'propertyschema_dict'} + Now: {'keyschema_dict': {'AAA': "value does not match regex '[a-z]+'"}} +- Error messages for `type` validations are different now (following + changes in Cerberus). +- It is no longer valid to have a field with `default` = None and + `nullable` = False. + (see patch.py:test_patch_nested_document_nullable_missing) +- See also: `Cerberus changes _` Stable ------ diff --git a/docs/config.rst b/docs/config.rst index 33f27947f..dda24d0e2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -370,9 +370,6 @@ uppercase. :ref:`unknown` for more information. Defaults to ``False``. -``TRANSPARENT_SCHEMA_RULES`` When ``True``, this option globally disables - :ref:`schema_validation` for any API endpoint. - ``PROJECTION`` When ``True``, this option enables the :ref:`projections` feature. Can be overridden by resource settings. Defaults @@ -1302,11 +1299,10 @@ defining the field validation rules. Allowed validation rules are: for all of which must validate with given schema. See `valueschema example `_. -``propertyschema`` This is the counterpart to ``valueschema`` that +``keyschema`` This is the counterpart to ``valueschema`` that validates the keys of a dict. Validation schema for all values of a ``dict``. See - `propertyschema example - `_. + `keyschema example `_. ``regex`` Validation will fail if field value does not diff --git a/docs/tutorials/custom_idfields.rst b/docs/tutorials/custom_idfields.rst index cae787339..af5ef5afb 100644 --- a/docs/tutorials/custom_idfields.rst +++ b/docs/tutorials/custom_idfields.rst @@ -82,13 +82,11 @@ details on custom validation): """ Extends the base mongo validator adding support for the uuid data-type """ - def _validate_type_uuid(self, field, value): + def _validate_type_uuid(self, value): try: UUID(value) except ValueError: - self._error(field, "value '%s' cannot be converted to a UUID" % - value) - + pass ``UUID`` URLs ~~~~~~~~~~~~~ diff --git a/docs/validation.rst b/docs/validation.rst index 53f670a26..7cf6c3a1e 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -97,16 +97,13 @@ code. .. code-block:: python - def _validate_type_objectid(self, field, value): + def _validate_type_objectid(self, value): """ Enables validation for `objectid` schema attribute. - :param unique: Boolean, whether the field value should be - unique or not. - :param field: field name. :param value: field value. """ - if not re.match('[a-f0-9]{24}', value): - self._error(field, ERROR_BAD_TYPE % 'ObjectId') + if isinstance(value, ObjectId): + return True This method enables support for MongoDB ``ObjectId`` type in your schema, allowing something like this: @@ -194,14 +191,8 @@ Schema validation By default, schemas are validated to ensure they conform to the structure documented in :ref:`schema`. -There are two ways to deal with non-conforming schemas: - -1. Add :ref:`custom_validation_rules` for non-conforming keys used in the - schema. - -2. Set the global option ``TRANSPARENT_SCHEMA_RULES`` to disable schema - validation globally or the resource option ``transparent_schema_rules`` - to disable schema validation for a given endpoint. +In order to deal with non-conforming schemas, add +:ref:`custom_validation_rules` for non-conforming keys used in the schema. .. _Cerberus: http://python-cerberus.org .. _`source code`: https://github.com/pyeve/eve/blob/master/eve/io/mongo/validation.py diff --git a/eve/default_settings.py b/eve/default_settings.py index f5049722a..732fc638b 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -238,9 +238,6 @@ # http://geojson.org/geojson-spec.html#geojson-objects ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False -# don't ignore unknown schema rules (raise SchemaError) -TRANSPARENT_SCHEMA_RULES = False - # Rate limits are disabled by default. Needs a running redis-server. RATE_LIMIT_GET = None RATE_LIMIT_POST = None diff --git a/eve/defaults.py b/eve/defaults.py deleted file mode 100644 index f0c0d4651..000000000 --- a/eve/defaults.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - Default values in schemas - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Default values for schemas work in two steps. - 1. The schema is searched for defaults and a list of default is built. - 2. In each POST/PUT request, for each default (if any) the document is - checked for a missing value, and if a value is missing the default is - added. - - :copyright: (c) 2017 by Nicola Iarocci. - :license: BSD, see LICENSE for more details. -""" - - -def build_defaults(schema): - """Build a tree of default values - - It walks the tree down looking for entries with a `default` key. In order - to avoid empty dicts the tree will be walked up and the empty dicts will be - removed. - - :param schema: Resource schema - :type schema: dict - :rtype: dict with defaults - - .. versionadded:: 0.4 - """ - # Pending schema nodes to process: loop and add defaults - pending = set() - # Stack of nodes to work on and clean up - stack = [(schema, None, None, {})] - level_schema, level_name, level_parent, current = stack[-1] - while len(stack) > 0: - leave = True - if isinstance(current, list): - level_schema = {'schema': level_schema.copy()} - for name, value in level_schema.items(): - default_next_level = None - if 'default' in value: - try: - current[name] = value['default'] - except TypeError: - current.append(value['default']) - elif value.get('type') == 'dict' and 'schema' in value: - default_next_level = {} - elif value.get('type') == 'list' and 'schema' in value: - default_next_level = [] - - if default_next_level is not None: - leave = False - next_level = add_next_level(name, current, default_next_level) - stack.append((value['schema'], name, current, next_level)) - pending.add(id(next_level)) - pending.discard(id(current)) - if leave: - # Leaves trigger the `walk up` till the next not processed node - while id(current) not in pending: - if not current and level_parent is not None: - try: - del level_parent[level_name] - except TypeError: - level_parent.remove(current) - stack.pop() - if len(stack) == 0: - break - level_schema, level_name, level_parent, current = stack[-1] - else: - level_schema, level_name, level_parent, current = stack[-1] - - return current - - -def add_next_level(name, current, default): - if isinstance(current, list): - current.append(default) - else: - default = current.setdefault(name, default) - return default - - -def resolve_default_values(document, defaults): - """ Add any defined default value for missing document fields. - - :param document: the document being posted or replaced - :param defaults: tree with the default values - :type defaults: dict - - .. versionchanged:: 0.5 - Fix #417. A default value of [] for a list causes an IndexError. - - .. versionadded:: 0.2 - """ - todo = [(defaults, document)] - while len(todo) > 0: - defaults, document = todo.pop() - if isinstance(defaults, list) and len(defaults): - todo.extend((defaults[0], item) for item in document) - continue - for name, value in defaults.items(): - if isinstance(value, dict): - # default dicts overwrite simple values - existing = document.setdefault(name, {}) - if not isinstance(existing, dict): - document[name] = {} - todo.append((value, document[name])) - if isinstance(value, list) and len(value): - existing = document.get(name) - if not existing: - document.setdefault(name, value) - continue - if all(isinstance(item, (dict, list)) for item in existing): - todo.extend((value[0], item) for item in existing) - else: - document.setdefault(name, existing) - else: - document.setdefault(name, value) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index e1fd90751..2fa9deef7 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -20,7 +20,6 @@ from werkzeug.serving import WSGIRequestHandler import eve -from eve.defaults import build_defaults from eve.endpoints import collections_endpoint, item_endpoint, home_endpoint, \ error_endpoint, media_endpoint, schema_collection_endpoint, \ schema_item_endpoint @@ -605,8 +604,6 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault('auth_field', self.config['AUTH_FIELD']) settings.setdefault('allow_unknown', self.config['ALLOW_UNKNOWN']) - settings.setdefault('transparent_schema_rules', - self.config['TRANSPARENT_SCHEMA_RULES']) settings.setdefault('extra_response_fields', self.config['EXTRA_RESPONSE_FIELDS']) settings.setdefault('mongo_write_concern', @@ -619,12 +616,6 @@ def _set_resource_defaults(self, resource, settings): schema = settings.setdefault('schema', {}) self.set_schema_defaults(schema, settings['id_field']) - # 'defaults' helper set contains the names of fields with default - # values in their schema definition. - - # TODO support default values for embedded documents. - settings['defaults'] = build_defaults(schema) - # list of all media fields for the resource settings['_media'] = [field for field, definition in schema.items() if definition.get('type') == 'media'] @@ -705,12 +696,6 @@ def _set_resource_projection(self, ds, schema, settings): ds['projection'] is not None: ds['projection'][self.config['DELETED']] = 1 - # 'defaults' helper set contains the names of fields with default - # values in their schema definition. - - # TODO support default values for embedded documents. - settings['defaults'] = build_defaults(schema) - # list of all media fields for the resource settings['_media'] = [field for field, definition in schema.items() if definition.get('type') == 'media' or diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 469ef808a..512205c5a 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -11,19 +11,17 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -import copy from bson import ObjectId from bson.dbref import DBRef -from collections import Mapping from flask import current_app as app from werkzeug.datastructures import FileStorage -from cerberus import Validator from eve.auth import auth_field_and_value from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \ MultiLineString, MultiPolygon, GeometryCollection, Feature, \ FeatureCollection -from eve.utils import config, str_type +from eve.utils import config +from eve.validation import Validator from eve.versioning import get_data_version_relation_document @@ -53,69 +51,12 @@ class Validator(Validator): Support for 'transparent_schema_rules' introduced with Cerberus 0.0.3, which allows for insertion of 'default' values in POST requests. """ - def __init__(self, schema=None, resource=None, allow_unknown=False, - transparent_schema_rules=False): - self.resource = resource - self._id = None - self._original_document = None - - if resource: - transparent_schema_rules = \ - config.DOMAIN[resource]['transparent_schema_rules'] - allow_unknown = config.DOMAIN[resource]['allow_unknown'] - super(Validator, self).__init__( - schema, - transparent_schema_rules=transparent_schema_rules, - allow_unknown=allow_unknown) - - def validate_update(self, document, _id, original_document=None): - """ Validate method to be invoked when performing an update, not an - insert. - - :param document: the document to be validated. - :param _id: the unique id of the document. - """ - self._id = _id - self._original_document = original_document - return super(Validator, self).validate_update(document) - - def validate_replace(self, document, _id, original_document=None): - """ Validation method to be invoked when performing a document - replacement. This differs from :func:`validation_update` since in this - case we want to perform a full :func:`validate` (the new document is to - be considered a new insertion and required fields needs validation). - However, like with validate_update, we also want the current _id - not to be checked when validating 'unique' values. - - .. versionadded:: 0.1.0 - """ - self._id = _id - self._original_document = original_document - return super(Validator, self).validate(document) - - def _validate_default(self, unique, field, value): - """ Fake validate function to let cerberus accept "default" - as keyword in the schema - - .. versionadded:: 0.6.2 - """ - pass - def _validate_versioned(self, unique, field, value): - """ Fake validate function to let cerberus accept "versioned" - as keyword in the schema - - .. versionadded:: 0.6.2 - """ + """ {'type': 'boolean'} """ pass def _validate_unique_to_user(self, unique, field, value): - """ Validates that a value is unique to the active user. Active user is - the user authenticated for current request. See #646. - - .. versionadded: 0.6 - """ - + """ {'type': 'boolean'} """ auth_field, auth_value = auth_field_and_value(self.resource) # if an auth value has been set for this request, then make sure it is @@ -125,24 +66,7 @@ def _validate_unique_to_user(self, unique, field, value): self._is_value_unique(unique, field, value, query) def _validate_unique(self, unique, field, value): - """ Enables validation for `unique` schema attribute. - - :param unique: Boolean, wether the field value should be - unique or not. - :param field: field name. - :param value: field value. - - .. versionchanged:: 0.6 - Validates field value uniqueness against the whole datasource, - independently of the request method. See #646. - - .. versionchanged:: 0.3 - Support for new 'self._error' signature introduced with Cerberus - v0.5. - - .. versionchanged:: 0.2 - Handle the case in which ID_FIELD is not of ObjectId type. - """ + """ {'type': 'boolean'} """ self._is_value_unique(unique, field, value, {}) def _is_value_unique(self, unique, field, value, query): @@ -174,9 +98,9 @@ def _is_value_unique(self, unique, field, value, query): query[config.DELETED] = {'$ne': True} # exclude current document - if self._id: + if self.document_id: id_field = resource_config['id_field'] - query[id_field] = {'$ne': self._id} + query[id_field] = {'$ne': self.document_id} # we perform the check on the native mongo driver (and not on # app.data.find_one()) because in this case we don't want the usual @@ -188,30 +112,13 @@ def _is_value_unique(self, unique, field, value, query): self._error(field, "value '%s' is not unique" % value) def _validate_data_relation(self, data_relation, field, value): - """ Enables validation for `data_relation` field attribute. Makes sure - 'value' of 'field' adheres to the referential integrity rule specified - by 'data_relation'. - - :param data_relation: a dict following keys: - 'resource': foreign resource name - 'field': foreign field name - 'version': True if this relation points to a specific version - 'type': the type for the reference field if 'version': True - :param field: field name. - :param value: field value. - - .. versionchanged:: 0.4 - Support for document versioning. - - .. versionchanged:: 0.3 - Support for new 'self._error' signature introduced with Cerberus - v0.5. - - .. versionchanged:: 0.1.1 - 'collection' key renamed to 'resource' (data_relation) - - .. versionadded: 0.0.5 - """ + """ {'type': 'dict', + 'schema': { + 'resource': {'type': 'string', 'required': True}, + 'field': {'type': 'string', 'required': True}, + 'embeddable': {'type': 'boolean', 'default': False}, + 'version': {'type': 'boolean', 'default': False} + }} """ if 'version' in data_relation and data_relation['version'] is True: value_field = data_relation['field'] version_field = app.config['VERSION'] @@ -256,227 +163,85 @@ def _validate_data_relation(self, data_relation, field, value): (item.id if isinstance(item, DBRef) else item, data_resource, data_relation['field'])) - def _validate_type_objectid(self, field, value): - """ Enables validation for `objectid` data type. - - :param field: field name. - :param value: field value. - - .. versionchanged:: 0.3 - Support for new 'self._error' signature introduced with Cerberus - v0.5. - - .. versionchanged:: 0.1.1 - regex check replaced with proper type check. - """ - if not isinstance(value, ObjectId): - self._error(field, "value '%s' cannot be converted to a ObjectId" - % value) - - def _validate_type_dbref(self, field, value): - """ Enables validation for `DBRef` data type. - - :param field: field name. - :param value: field value. - - """ - if not isinstance(value, DBRef): - self._error(field, "value '%s' cannot be converted to a DBRef" - % value) - - def _validate_readonly(self, read_only, field, value): - """ - .. versionchanged:: 0.5 - Not taking default values in consideration anymore since they are - now being resolved after validation (#353). - Consider the original value if available (#479). - - .. versionadded:: 0.4 - """ - original_value = self._original_document.get(field) \ - if self._original_document else None - if value != original_value: - super(Validator, self)._validate_readonly(read_only, field, value) - - def _validate_dependencies(self, document, dependencies, field, - break_on_error=False): - """ With PATCH method, the validator is only provided with the updated - fields. If an updated field depends on another field in order to be - edited and the other field was previously set, the validator doesn't - see it and rejects the update. In order to avoid that we merge the - proposed changes with the original document before validating - dependencies. - - .. versionchanged:: 0.6.1 - Fix: dependencies on sub-document fields are now properly - processed (#706). - - .. versionchanged:: 0.6 - Fix: Only evaluate dependencies that don't have valid default - values. - - .. versionchanged:: 0.5.1 - Fix: dependencies with value checking seems broken #547. - - .. versionadded:: 0.5 - If a dependency has a default value, skip it as Cerberus does not - have the notion of default values and would report a missing - dependency (#353). - Fix for #363 (see docstring). - """ - if dependencies is None: + def _validate_type_objectid(self, value): + if isinstance(value, ObjectId): return True - if isinstance(dependencies, str_type): - dependencies = [dependencies] - - defaults = {} - for d in dependencies: - root = d.split('.')[0] - default = self.schema[root].get('default') - if default and root not in document: - defaults[root] = default - - if isinstance(dependencies, Mapping): - # Only evaluate dependencies that don't have *valid* defaults - for k, v in defaults.items(): - if v in dependencies[k]: - del(dependencies[k]) - else: - # Only evaluate dependencies that don't have defaults values - dependencies = [d for d in dependencies if d not in - defaults.keys()] - - dcopy = None - if self._original_document: - dcopy = copy.copy(document) - dcopy.update(self._original_document) - return super(Validator, self)._validate_dependencies(dcopy or document, - dependencies, - field, - break_on_error) - - def _validate_type_media(self, field, value): - """ Enables validation for `media` data type. - - :param field: field name. - :param value: field value. - - .. versionadded:: 0.3 - """ - if not isinstance(value, FileStorage): - self._error(field, "file was expected, got '%s' instead." % value) + def _validate_type_dbref(self, value): + if isinstance(value, DBRef): + return True - def _validate_type_point(self, field, value): - """ Enables validation for `point` data type. + def _validate_type_media(self, value): + if isinstance(value, FileStorage): + return True - :param field: field name. - :param value: field value. - """ + def _validate_type_point(self, value): try: Point(value) - except TypeError as e: - self._error(field, "Point not correct %s: %s" % (value, e)) - - def _validate_type_linestring(self, field, value): - """ Enables validation for `linestring` data type. + return True + except TypeError: + pass - :param field: field name. - :param value: field value. - """ + def _validate_type_linestring(self, value): try: LineString(value) + return True except TypeError: - self._error(field, "LineString not correct %s " % value) - - def _validate_type_polygon(self, field, value): - """ Enables validation for `polygon` data type. + pass - :param field: field name. - :param value: field value. - """ + def _validate_type_polygon(self, value): try: Polygon(value) + return True except TypeError: - self._error(field, "LineString not correct %s " % value) + pass - def _validate_type_multipoint(self, field, value): - """ Enables validation for `multipoint` data type. - - :param field: field name. - :param value: field value. - """ + def _validate_type_multipoint(self, value): try: MultiPoint(value) + return True except TypeError: - self._error(field, "MultiPoint not correct" % value) - - def _validate_type_multilinestring(self, field, value): - """ Enables validation for `multilinestring`data type. + pass - :param field: field name. - :param value: field value. - """ + def _validate_type_multilinestring(self, value): try: MultiLineString(value) + return True except TypeError: - self._error(field, "MultiLineString not correct" % value) - - def _validate_type_multipolygon(self, field, value): - """ Enables validation for `multipolygon` data type. + pass - :param field: field name. - :param value: field value. - """ + def _validate_type_multipolygon(self, value): try: MultiPolygon(value) + return True except TypeError: - self._error(field, "MultiPolygon not correct" % value) + pass - def _validate_type_geometrycollection(self, field, value): - """ Enables validation for `geometrycollection`data type - - :param field: field name. - :param value: field nvalue - """ + def _validate_type_geometrycollection(self, value): try: GeometryCollection(value) + return True except TypeError: - self._error(field, "GeometryCollection not correct" % value) + pass - def _validate_type_feature(self, field, value): + def _validate_type_feature(self, value): """ Enables validation for `feature`data type - :param field: field name. - :param value: field nvalue + :param value: field value """ try: Feature(value) + return True except TypeError: - self._error(field, "Feature not correct" % value) + pass - def _validate_type_featurecollection(self, field, value): + def _validate_type_featurecollection(self, value): """ Enables validation for `featurecollection`data type - :param field: field name. - :param value: field nvalue + :param value: field value """ try: FeatureCollection(value) + return True except TypeError: - self._error(field, "FeatureCollection not correct" % value) - - def _error(self, field, _error): - """ Change the default behaviour so that, if VALIDATION_ERROR_AS_LIST - is enabled, single validation errors are returned as a list. See #536. - - :param field: field name - :param _error: field error(s) - - .. versionadded:: 0.6 - """ - super(Validator, self)._error(field, _error) - if config.VALIDATION_ERROR_AS_LIST: - err = self._errors[field] - if not isinstance(err, list): - self._errors[field] = [err] + pass diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 8b88438e5..d63c9a795 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -16,7 +16,7 @@ from datetime import datetime from eve.utils import config, debug_error_message, parse_request from eve.auth import requires_auth -from eve.validation import ValidationError +from eve.validation import DocumentError from eve.methods.common import get_document, parse, payload as payload_, \ ratelimit, pre_event, store_media_files, resolve_embedded_fields, \ build_response_document, marshal_write_response, resolve_document_etag, \ @@ -138,7 +138,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] - validator = app.validator(schema, resource) + validator = app.validator(schema, resource=resource) object_id = original[resource_def['id_field']] last_modified = None @@ -223,7 +223,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, etag = response[config.ETAG] else: issues = validator.errors - except ValidationError as e: + except DocumentError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues['validator exception'] = str(e) diff --git a/eve/methods/post.py b/eve/methods/post.py index e0375ce2a..59a1b56df 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -15,8 +15,7 @@ from flask import current_app as app, abort from eve.utils import config, parse_request, debug_error_message from eve.auth import requires_auth -from eve.defaults import resolve_default_values -from eve.validation import ValidationError +from eve.validation import DocumentError from eve.methods.common import parse, payload, ratelimit, \ pre_event, store_media_files, resolve_user_restricted_access, \ resolve_embedded_fields, build_response_document, marshal_write_response, \ @@ -151,7 +150,8 @@ def post_internal(resource, payl=None, skip_validation=False): date_utc = datetime.utcnow().replace(microsecond=0) resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] - validator = None if skip_validation else app.validator(schema, resource) + validator = None if skip_validation \ + else app.validator(schema, resource=resource) documents = [] results = [] failures = 0 @@ -205,14 +205,13 @@ def post_internal(resource, payl=None, skip_validation=False): document[config.DELETED] = False resolve_user_restricted_access(document, resource) - resolve_default_values(document, resource_def['defaults']) store_media_files(document, resource) resolve_document_version(document, resource, 'POST') else: # validation errors added to list of document issues doc_issues = validator.errors - except ValidationError as e: - doc_issues['validator exception'] = str(e) + except DocumentError as e: + doc_issues['validation exception'] = str(e) except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue diff --git a/eve/methods/put.py b/eve/methods/put.py index fdca5c5a9..d07dff549 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -15,14 +15,13 @@ from werkzeug import exceptions from eve.auth import requires_auth -from eve.defaults import resolve_default_values from eve.methods.common import get_document, parse, payload as payload_, \ ratelimit, pre_event, store_media_files, resolve_user_restricted_access, \ resolve_embedded_fields, build_response_document, marshal_write_response, \ resolve_sub_resource_path, resolve_document_etag, oplog_push from eve.methods.post import post_internal from eve.utils import config, debug_error_message, parse_request -from eve.validation import ValidationError +from eve.validation import DocumentError from eve.versioning import resolve_document_version, \ insert_versioning_documents, late_versioning_catch @@ -109,7 +108,7 @@ def put_internal(resource, payload=None, concurrency_check=False, """ resource_def = app.config['DOMAIN'][resource] schema = resource_def['schema'] - validator = app.validator(schema, resource) + validator = app.validator(schema, resource=resource) if payload is None: payload = payload_() @@ -172,7 +171,6 @@ def put_internal(resource, payload=None, concurrency_check=False, document[resource_def['id_field']] = object_id resolve_user_restricted_access(document, resource) - resolve_default_values(document, resource_def['defaults']) store_media_files(document, resource, original) resolve_document_version(document, resource, 'PUT', original) @@ -208,7 +206,7 @@ def put_internal(resource, payload=None, concurrency_check=False, etag = response[config.ETAG] else: issues = validator.errors - except ValidationError as e: + except DocumentError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) issues['validator exception'] = str(e) diff --git a/eve/tests/config.py b/eve/tests/config.py index 6e2566557..d02a41991 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -265,8 +265,6 @@ def _test_defaults_for_resource(self, resource): self.app.config['AUTH_FIELD']) self.assertEqual(settings['allow_unknown'], self.app.config['ALLOW_UNKNOWN']) - self.assertEqual(settings['transparent_schema_rules'], - self.app.config['TRANSPARENT_SCHEMA_RULES']) self.assertEqual(settings['extra_response_fields'], self.app.config['EXTRA_RESPONSE_FIELDS']) self.assertEqual(settings['mongo_write_concern'], @@ -344,24 +342,6 @@ def assertValidateSchemaFailure(self, resource, schema, expected): else: self.fail("SchemaException expected but not raised.") - def test_schema_defaults(self): - self.domain.clear() - self.domain['resource'] = { - 'schema': { - 'title': { - 'type': 'string', - 'default': 'Mr.', - }, - 'price': { - 'type': 'integer', - 'default': 100 - }, - } - } - self.app.set_defaults() - settings = self.domain['resource'] - self.assertEqual({'title': 'Mr.', 'price': 100}, settings['defaults']) - def test_url_helpers(self): self.assertNotEqual(self.app.config.get('URLS'), None) self.assertEqual(type(self.app.config['URLS']), dict) diff --git a/eve/tests/default_values.py b/eve/tests/default_values.py deleted file mode 100644 index 4da7a97aa..000000000 --- a/eve/tests/default_values.py +++ /dev/null @@ -1,243 +0,0 @@ -import unittest - -from eve.defaults import build_defaults, resolve_default_values - - -class TestBuildDefaults(unittest.TestCase): - def test_schemaless_dict(self): - schema = { - "address": { - 'type': 'dict' - } - } - self.assertEqual({}, build_defaults(schema)) - - def test_simple(self): - schema = { - "name": {'type': 'string'}, - "email": {'type': 'string', 'default': "no@example.com"} - } - res = build_defaults(schema) - self.assertEqual({'email': 'no@example.com'}, res) - - def test_nested_one_level(self): - schema = { - "address": { - 'type': 'dict', - 'schema': { - 'street': {'type': 'string'}, - 'country': {'type': 'string', 'default': 'wonderland'} - } - } - } - res = build_defaults(schema) - self.assertEqual({'address': {'country': 'wonderland'}}, res) - - def test_empty_defaults_multiple_level(self): - schema = { - 'subscription': { - 'type': 'dict', - 'schema': { - 'type': {'type': 'string'}, - 'when': { - 'type': 'dict', - 'schema': { - 'timestamp': {'type': 'int'}, - 'repr': {'type': 'string'} - } - } - } - } - } - res = build_defaults(schema) - self.assertEqual({}, res) - - def test_nested_multilevel(self): - schema = { - "subscription": { - 'type': 'dict', - 'schema': { - 'type': {'type': 'string'}, - 'when': { - 'type': 'dict', - 'schema': { - 'timestamp': {'type': 'int', 'default': 0}, - 'repr': {'type': 'string', 'default': '0'} - } - } - } - } - } - res = build_defaults(schema) - self.assertEqual( - {'subscription': {'when': {'timestamp': 0, 'repr': '0'}}}, - res) - - def test_default_in_list_schema(self): - schema = { - "one": { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - 'title': { - 'type': 'string', - 'default': 'M.' - } - } - } - }, - "two": { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - 'name': {'type': 'string'} - } - } - } - } - res = build_defaults(schema) - self.assertEqual({"one": [{'title': 'M.'}]}, res) - - def test_default_in_list_without_schema(self): - schema = { - "one": { - 'type': 'list', - 'schema': { - 'type': 'string', - 'default': 'item' - } - } - } - res = build_defaults(schema) - self.assertEqual({"one": ['item']}, res) - - def test_lists_of_lists_with_default(self): - schema = { - 'twisting': { - 'type': 'list', # list of groups - 'required': True, - 'schema': { - 'type': 'list', # list of signals (in one group) - 'schema': { - 'type': 'string', - 'default': 'listoflist', - } - } - } - } - res = build_defaults(schema) - self.assertEqual({'twisting': [['listoflist']]}, res) - - def test_lists_of_lists_without_default(self): - schema = { - 'twisting': { - 'type': 'list', # list of groups - 'required': True, - 'schema': { - 'type': 'list', # list of signals (in one group) - 'schema': { - 'type': 'ObjectId', - 'required': True - } - } - } - } - res = build_defaults(schema) - self.assertEqual({}, res) - - def test_lists_of_lists_with_a_dict(self): - schema = { - 'twisting': { - 'type': 'list', # list of groups - 'required': True, - 'schema': { - 'type': 'list', # list of signals (in one group) - 'schema': { - 'type': 'dict', - 'schema': { - 'name': { - 'type': 'string', - 'default': 'me' - } - }, - } - } - } - } - res = build_defaults(schema) - self.assertEqual({'twisting': [[{'name': 'me'}]]}, res) - - -class TestResolveDefaultValues(unittest.TestCase): - def test_one_level(self): - document = {'name': 'john'} - defaults = {'email': 'noemail'} - resolve_default_values(document, defaults) - self.assertEqual({'name': 'john', 'email': 'noemail'}, document) - - def test_multilevel(self): - document = {'name': 'myname', 'one': {'hey': 'jude'}} - defaults = {'one': {'two': {'three': 'banana'}}} - resolve_default_values(document, defaults) - expected = { - 'name': 'myname', - 'one': { - 'hey': 'jude', - 'two': {'three': 'banana'} - } - } - self.assertEqual(expected, document) - - def test_value_instead_of_dict(self): - document = {'name': 'john'} - defaults = {'name': {'first': 'john'}} - resolve_default_values(document, defaults) - self.assertEqual(document, defaults) - - def test_lists(self): - document = {"one": [{"name": "john"}, {}]} - defaults = {"one": [{"title": "M."}]} - resolve_default_values(document, defaults) - expected = {"one": [ - {"name": "john", "title": "M."}, - {"title": "M."}]} - self.assertEqual(expected, document) - - def test_list_of_list_single_value(self): - document = {'one': [[], []]} - defaults = {'one': [['listoflist']]} - resolve_default_values(document, defaults) - # This functionality is not supported, no change in the document - expected = {'one': [[], []]} - assert expected == document - - def test_list_empty_list_as_default(self): - # test that a default value of [] for a list does not causes IndexError - # (#417). - document = {'a': ['b']} - defaults = {'a': []} - resolve_default_values(document, defaults) - expected = {'a': ['b']} - assert expected == document - - def test_list_of_strings_as_default(self): - document = {} - defaults = {'a': ['b']} - resolve_default_values(document, defaults) - expected = {'a': ['b']} - assert expected == document - # overwrite defaults - document = {'a': ['c', 'd']} - defaults = {'a': ['b']} - resolve_default_values(document, defaults) - expected = {'a': ['c', 'd']} - assert expected == document - - def test_list_of_list_dict_value(self): - document = {'one': [[{}], [{}]]} - defaults = {'one': [[{'name': 'banana'}]]} - resolve_default_values(document, defaults) - expected = {'one': [[{'name': 'banana'}], [{'name': 'banana'}]]} - assert expected == document diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 46a8e0c0a..d6152fee3 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -45,12 +45,12 @@ class UUIDValidator(Validator): """ Extends the base mongo validator adding support for the uuid data-type """ - def _validate_type_uuid(self, field, value): + def _validate_type_uuid(self, value): try: UUID(value) + return True except ValueError: - self._error("value '%s' for field '%s' cannot be converted to a " - "UUID" % (value, field)) + pass class TestCustomConverters(TestMinimal): diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 1380bb2fa..b67e13456 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -48,7 +48,7 @@ def test_gridfs_media_storage_post(self): self.assertEqual(STATUS_ERR, r[STATUS]) # validates media fields - self.assertTrue('file was expected' in r[ISSUES]['media']) + self.assertTrue('must be of media type' in r[ISSUES]['media']) # also validates ordinary fields self.assertTrue('required' in r[ISSUES][self.test_field]) @@ -80,7 +80,7 @@ def test_gridfs_media_storage_post_excluded_file_in_result(self): self.assertEqual(STATUS_ERR, r[STATUS]) # validates media fields - self.assertTrue('file was expected' in r[ISSUES]['media']) + self.assertTrue('must be of media type' in r[ISSUES]['media']) # also validates ordinary fields self.assertTrue('required' in r[ISSUES][self.test_field]) diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index 378322f1f..a15c980fd 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -100,7 +100,7 @@ def test_objectid_fail(self): v = Validator(schema, None) self.assertFalse(v.validate(doc)) self.assertTrue('id' in v.errors) - self.assertTrue('ObjectId' in v.errors['id']) + self.assertTrue('objectid' in v.errors['id']) def test_objectid_success(self): schema = {'id': {'type': 'objectid'}} @@ -114,7 +114,7 @@ def test_dbref_fail(self): v = Validator(schema, None) self.assertFalse(v.validate(doc)) self.assertTrue('id' in v.errors) - self.assertTrue('DBRef' in v.errors['id']) + self.assertTrue('dbref' in v.errors['id']) def test_dbref_success(self): schema = {'id': {'type': 'dbref'}} @@ -123,31 +123,17 @@ def test_dbref_success(self): v = Validator(schema, None) self.assertTrue(v.validate(doc)) - def test_transparent_rules(self): - schema = {'a_field': {'type': 'string'}} - v = Validator(schema) - self.assertFalse(v.transparent_schema_rules) - def test_reject_invalid_schema(self): schema = {'a_field': {'foo': 'bar'}} self.assertRaises(SchemaError, lambda: Validator(schema)) - def test_enable_transparent_rules(self): - schema = {'a_field': {'type': 'string'}} - v = Validator(schema, transparent_schema_rules=True) - self.assertTrue(v.transparent_schema_rules) - - def test_transparent_rules_accept_invalid_schema(self): - schema = {'a_field': {'foo': 'bar'}} - Validator(schema, transparent_schema_rules=True) - def test_geojson_not_compilant(self): schema = {'location': {'type': 'point'}} doc = {'location': [10.0, 123.0]} v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('Point' in v.errors['location']) + self.assertTrue('point' in v.errors['location']) def test_geometry_not_compilant(self): schema = {'location': {'type': 'point'}} @@ -155,7 +141,7 @@ def test_geometry_not_compilant(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('Point' in v.errors['location']) + self.assertTrue('point' in v.errors['location']) def test_geometrycollection_not_compilant(self): schema = {'location': {'type': 'geometrycollection'}} @@ -164,7 +150,7 @@ def test_geometrycollection_not_compilant(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('GeometryCollection' in v.errors['location']) + self.assertTrue('geometrycollection' in v.errors['location']) def test_point_success(self): schema = {'location': {'type': 'point'}} @@ -178,7 +164,7 @@ def test_point_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('Point' in v.errors['location']) + self.assertTrue('point' in v.errors['location']) def test_point_coordinates_fail(self): schema = {'location': {'type': 'point'}} @@ -186,7 +172,7 @@ def test_point_coordinates_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('Point' in v.errors['location']) + self.assertTrue('point' in v.errors['location']) def test_point_integer_success(self): schema = {'location': {'type': 'point'}} @@ -209,7 +195,7 @@ def test_linestring_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('LineString' in v.errors['location']) + self.assertTrue('linestring' in v.errors['location']) def test_polygon_success(self): schema = {'location': {'type': 'polygon'}} @@ -231,7 +217,7 @@ def test_polygon_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('location' in v.errors) - self.assertTrue('Polygon' in v.errors['location']) + self.assertTrue('polygon' in v.errors['location']) def test_multipoint_success(self): schema = {'location': {'type': 'multipoint'}} @@ -296,7 +282,7 @@ def test_geometrycollection_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('locations' in v.errors) - self.assertTrue('GeometryCollection' in v.errors['locations']) + self.assertTrue('geometrycollection' in v.errors['locations']) def test_feature_success(self): schema = {'locations': {'type': 'feature'}} @@ -325,7 +311,7 @@ def test_feature_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('locations' in v.errors) - self.assertTrue('Feature' in v.errors['locations']) + self.assertTrue('feature' in v.errors['locations']) def test_featurecollection_success(self): schema = {'locations': {'type': 'featurecollection'}} @@ -350,7 +336,7 @@ def test_featurecollection_fail(self): v = Validator(schema) self.assertFalse(v.validate(doc)) self.assertTrue('locations' in v.errors) - self.assertTrue('FeatureCollection' in v.errors['locations']) + self.assertTrue('featurecollection' in v.errors['locations']) def test_dependencies_with_defaults(self): schema = { diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index b28e34948..1963470b5 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -181,20 +181,28 @@ def test_patch_null_objectid(self): db_value = self.compare_patch_with_get(field, r) self.assertEqual(db_value, test_value) - def test_patch_defaults(self): + def test_patch_missing_default(self): + """ PATCH an object which is missing a field with a default value. + + This should result in setting the field to its default value, even if + the field is not provided in the PATCH's payload. """ field = "ref" test_value = "1234567890123456789012345" changes = {field: test_value} r = self.perform_patch(changes) - self.assertRaises(KeyError, self.compare_patch_with_get, 'title', r) + self.assertEqual(self.compare_patch_with_get('title', r), 'Mr.') + + def test_patch_missing_default_with_post_override(self): + """ PATCH an object which is missing a field with a default value. - def test_patch_defaults_with_post_override(self): + This should result in setting the field to its default value, even if + the field is not provided in the PATCH's payload. """ field = "ref" test_value = "1234567890123456789012345" r = self.perform_patch_with_post_override(field, test_value) self.assert200(r.status_code) - self.assertRaises(KeyError, self.compare_patch_with_get, 'title', - json.loads(r.get_data())) + title = self.compare_patch_with_get('title', json.loads(r.get_data())) + self.assertEqual(title, 'Mr.') def test_patch_multiple_fields(self): fields = ['ref', 'prog', 'role'] @@ -544,6 +552,7 @@ def test_patch_nested_document_nullable_missing(self): 'name': {'type': 'string'}, }, 'default': None, + 'nullable': True }, 'other': { 'type': 'dict', @@ -586,7 +595,6 @@ def test_patch_dependent_field_on_origin_document(self): # this will fail as dependent field is missing even in the # document we are trying to update. del(self.domain['contacts']['schema']['dependency_field1']['default']) - del(self.domain['contacts']['defaults']['dependency_field1']) changes = {'dependency_field2': 'value'} r, status = self.patch(self.item_id_url, data=changes, headers=[('If-Match', self.item_etag)]) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index d34e3fc02..a05fb3dc3 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -208,7 +208,7 @@ def test_multi_post_invalid(self): self.assertValidationError(results[1], {'ref': 'required'}) self.assertValidationError(results[3], {'ref': 'unique'}) - self.assertValidationError(results[4], {'tid': 'ObjectId'}) + self.assertValidationError(results[4], {'tid': 'objectid'}) id_field = self.domain[self.known_resource]['id_field'] self.assertTrue(id_field not in results[0]) @@ -781,21 +781,21 @@ def test_post_valueschema_dict(self): data={"valueschema_dict": {"k1": 1}}) self.assert201(status) - def test_post_propertyschema_dict(self): + def test_post_keyschema_dict(self): del(self.domain['contacts']['schema']['ref']['required']) r, status = self.post(self.known_resource_url, - data={"propertyschema_dict": {"aaa": 1}}) + data={"keyschema_dict": {"aaa": 1}}) self.assert201(status) r, status = self.post(self.known_resource_url, - data={"propertyschema_dict": {"AAA": "1"}}) + data={"keyschema_dict": {"AAA": "1"}}) self.assertValidationErrorStatus(status) issues = r[ISSUES] - self.assertTrue('propertyschema_dict' in issues) - self.assertEqual(issues['propertyschema_dict'], - 'propertyschema_dict') + self.assertTrue('keyschema_dict' in issues) + self.assertEqual(issues['keyschema_dict'], + {'AAA': "value does not match regex '[a-z]+'"}) def test_post_internal(self): # test that post_internal is available and working properly. diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 425b40355..9dd2363bc 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -130,9 +130,9 @@ 'key1': { 'type': 'string', }, - 'propertyschema_dict': { + 'keyschema_dict': { 'type': 'dict', - 'propertyschema': {'type': 'string', 'regex': '[a-z]+'} + 'keyschema': {'type': 'string', 'regex': '[a-z]+'} }, 'valueschema_dict': { 'type': 'dict', diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index f57e5d80a..377662ab0 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -901,8 +901,7 @@ def test_referential_integrity(self): r, status = self.post('/invoices/', data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': { - value_field: "value 'bad' cannot be converted to a ObjectId"}}) + r, {'person': {value_field: "must be of objectid type"}}) # unknown id data = {"person": { diff --git a/eve/validation.py b/eve/validation.py index 88720bb96..577a81ced 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -12,5 +12,145 @@ :license: BSD, see LICENSE for more details. """ -# flake8: noqa -from cerberus import ValidationError, SchemaError +import copy +import cerberus +import cerberus.errors +from cerberus import DocumentError, SchemaError # flake8: noqa + +from eve.utils import config + + +class Validator(cerberus.Validator): + + def __init__(self, *args, **kwargs): + if not config.VALIDATION_ERROR_AS_LIST: + kwargs['error_handler'] = SingleErrorAsStringErrorHandler + + resource = kwargs.get('resource', None) + if resource: + resource_def = config.DOMAIN[resource] + kwargs['allow_unknown'] = resource_def['allow_unknown'] + super(Validator, self).__init__(*args, **kwargs) + + def validate_update(self, document, document_id, persisted_document=None): + """ Validate method to be invoked when performing an update, not an + insert. + + :param document: the document to be validated. + :param document_id: the unique id of the document. + :param persisted_document: the persisted document to be updated. + """ + self.document_id = document_id + self.persisted_document = persisted_document + return super(Validator, self).validate(document, update=True) + + def validate_replace(self, document, document_id, persisted_document=None): + """ Validation method to be invoked when performing a document + replacement. This differs from :func:`validation_update` since in this + case we want to perform a full :func:`validate` (the new document is to + be considered a new insertion and required fields needs validation). + However, like with validate_update, we also want the current document_id + not to be checked when validating 'unique' values. + + :param document: the document to be validated. + :param document_id: the unique id of the document. + :param persisted_document: the persisted document to be updated. + + .. versionadded:: 0.1.0 + """ + self.document_id = document_id + self.persisted_document = persisted_document + return super(Validator, self).validate(document) + + def _normalize_default(self, mapping, schema, field): + """ {'nullable': True} """ + if not self.persisted_document or \ + field not in self.persisted_document: + super(Validator, self)._normalize_default(mapping, schema, field) + + def _normalize_default_setter(self, mapping, schema, field): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'string'} + ]} """ + if not self.persisted_document or \ + field not in self.persisted_document: + super(Validator, self)._normalize_default_setter(mapping, schema, + field) + + def _validate_dependencies(self, dependencies, field, value): + """ {'type': ['dict', 'hashable', 'hashables']} """ + persisted = self._filter_persisted_fields_not_in_document(dependencies) + if persisted: + dcopy = copy.copy(self.document) + for field in persisted: + dcopy[field] = self.persisted_document[field] + validator = self._get_child_validator() + validator.validate(dcopy, update=self.update) + self._error(validator._errors) + else: + super(Validator, self)._validate_dependencies(dependencies, field, + value) + + def _filter_persisted_fields_not_in_document(self, fields): + def persisted_but_not_in_document(field): + return field not in self.document and \ + self.persisted_document and \ + field in self.persisted_document + return [field for field in fields if + persisted_but_not_in_document(field)] + + def _validate_readonly(self, read_only, field, value): + """ {'type': 'boolean'} """ + persisted_value = self.persisted_document.get(field) \ + if self.persisted_document else None + if value != persisted_value: + super(Validator, self)._validate_readonly(read_only, field, value) + + @property + def resource(self): + return self._config.get('resource', None) + + @resource.setter + def resource(self, value): + self._config['resource'] = value + + @property + def document_id(self): + return self._config.get('document_id', None) + + @document_id.setter + def document_id(self, value): + self._config['document_id'] = value + + @property + def persisted_document(self): + return self._config.get('persisted_document', None) + + @persisted_document.setter + def persisted_document(self, value): + self._config['persisted_document'] = value + + +class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler): + """ Default Cerberus error handler for Eve. + + Since Cerberus 1.0, error messages for fields will always be returned as + lists, even in the case of a single error. To maintain compatibility with + clients, this error handler will unpack single-element error lists unless + the config item VALIDATION_ERROR_AS_LIST is True. + """ + + @property + def pretty_tree(self): + pretty = super(SingleErrorAsStringErrorHandler, self).pretty_tree + self._unpack_single_element_lists(pretty) + return pretty + + def _unpack_single_element_lists(self, tree): + for field in tree: + error_list = tree[field] + if len(error_list) > 0 and isinstance(tree[field][-1], dict): + self._unpack_single_element_lists(tree[field][-1]) + if len(tree[field]) == 1: + tree[field] = tree[field][0] diff --git a/requirements.txt b/requirements.txt index 7b67198e2..1df23c80e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Cerberus==0.9.2 +Cerberus==1.1 Events==0.2.1 Flask==0.12 itsdangerous==0.24 diff --git a/setup.py b/setup.py index 017646734..4f675653c 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ LONG_DESCRIPTION = f.read() install_requires = [ - 'cerberus>=0.9.2,<0.10', + 'cerberus>=1.1', 'events>=0.2.1,<0.3', 'simplejson>=3.3.0,<4.0', 'werkzeug>=0.9.4,<=0.11.15', From c781c4885e407b9c8ca89c0084263cc5ef90c5cc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 25 May 2017 09:10:03 +0200 Subject: [PATCH 188/821] Brad P. Crochet --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 701f0c51a..d1269ac75 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,6 +18,7 @@ Patches and Contributions - Ashley Roach - Ben Demaree - Bjorn Andersson +- Brad P. Crochet - Brian Mego - Bryan Cattle - Carles Bruguera From 44f45ef56f0fca13496dbbcbf92dc85a73060bb0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 25 May 2017 09:47:07 +0200 Subject: [PATCH 189/821] changelog: refactor Cerberus 1.0 breaking changes --- CHANGES | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index 977b21665..1f8124fc1 100644 --- a/CHANGES +++ b/CHANGES @@ -12,38 +12,33 @@ Version 0.8 (Martin Fous). - New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. Closes #769 (Martin Fous). -- Dropped Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). - Config options ``MONGO_AUTH_MECHANISM`` and ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. +- Change: Support for Cerberus 1.0+. Closes #776 (Dominik Kellner, Brad P. Crochet). +- Change: Drop Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). +- Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). - Dev: after the latest update (May 4th) travis-ci would not run tests on Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. -- Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). Breaking Changes ................ +- Eve now relies on `Cerberus `_ 1.0+, which allows for many new powerful validation and trasformation features (like `schema registries `_), improved performance and, in general, a more streamlined API. It also brings some notable breaking changes. + - ``keyschema`` was renamed to ``valueschema``, and ``propertyschema`` to ``keyschema``. + - A PATCH on a document which misses a field having a default value will now result in setting this value, even if the field was not provided in the PATCH's payload. + - Error messages for ``keyschema`` are now returned as dictionary. Example: ``{'a_dict': {'a_field': "value does not match regex '[a-z]+'"}}``. + - Error messages for type validations are `different now `_. + - It is no longer valid to have a field with ``default = None`` and ``nullable = False`` (see patch.py:test_patch_nested_document_nullable_missing). + - And more. A complete list of breaking changes is available `here `_. For detailed upgrade instructions, see Cerberus `upgrade notes `_. An in-depth analysis of changes made to the codebase (useful if you wrote a custom validator which needs to be upgraded) is available with `this commit message `_. + - Special thanks to Dominik Kellner and Brad P. Crochet for the amazing job done on this upgrade. - Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` for naming consistency with PyMongo. - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URI`` instead. -- `keyschema` was renamed to `valueschema` and `propertyschema` to - `keyschema` (following changes in Cerberus). -- A PATCH on a document which misses a field having a default value will - now result in setting this value, even if the field was not provided - in the PATCH's payload. -- Error messages for `keyschema` are now returned as dictionary. - Before: {'propertyschema_dict': 'propertyschema_dict'} - Now: {'keyschema_dict': {'AAA': "value does not match regex '[a-z]+'"}} -- Error messages for `type` validations are different now (following - changes in Cerberus). -- It is no longer valid to have a field with `default` = None and - `nullable` = False. - (see patch.py:test_patch_nested_document_nullable_missing) -- See also: `Cerberus changes _` Stable ------ From 3c8f6f382db68da38693b276b932c402c68928c1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 3 Jun 2017 10:33:15 +0200 Subject: [PATCH 190/821] Bump Events dependency to v0.3+ --- CHANGES | 1 + requirements.txt | 3 ++- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 1f8124fc1..48dcac90f 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,7 @@ Version 0.8 Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. +- Update: Upgrade Events dependency to v0.3. Breaking Changes ................ diff --git a/requirements.txt b/requirements.txt index 1df23c80e..c7ba8127b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Cerberus==1.1 -Events==0.2.1 +Cerberus==0.9.2 +Events==0.3 Flask==0.12 itsdangerous==0.24 Jinja2==2.9.4 diff --git a/setup.py b/setup.py index 4f675653c..9c7509fa2 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ install_requires = [ 'cerberus>=1.1', - 'events>=0.2.1,<0.3', + 'events>=0.3,<0.4', 'simplejson>=3.3.0,<4.0', 'werkzeug>=0.9.4,<=0.11.15', 'markupsafe>=0.23,<1.0', From 3231fe2e0288874e360f5b51fc8d86a52f625db2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Jun 2017 09:23:32 +0200 Subject: [PATCH 191/821] Duplicated Cerberus entry in requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c7ba8127b..7081c0442 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Cerberus==1.1 -Cerberus==0.9.2 Events==0.3 Flask==0.12 itsdangerous==0.24 From 314db63e67177d9226de9fd1c8a5fe9a9eb110da Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Jun 2017 09:24:22 +0200 Subject: [PATCH 192/821] Remove flask-pymongo from setup.py's install_requires --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 9c7509fa2..0ca89a5e8 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ 'itsdangerous>=0.24,<1.0', 'flask>=0.10.1,<=0.12', 'pymongo>=3.4', - 'flask-pymongo>=0.4', ] try: From c8fa52bb65b0ad0801df569fa7b3f0554e5474d2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Jun 2017 15:30:08 +0200 Subject: [PATCH 193/821] Bump version to 0.8-dev --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 776adfa0d..bb577c84e 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.4' +__version__ = '0.8-dev' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 0ca89a5e8..830136e90 100755 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name='Eve', - version='0.7.4', + version='0.8-dev', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 7ec0e9b070ff9ee1f60f3ab50048024233ed6a84 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 6 Jun 2017 15:21:18 +0200 Subject: [PATCH 194/821] minor cleanup --- eve/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/render.py b/eve/render.py index 3e3b2f8d6..aea46317e 100644 --- a/eve/render.py +++ b/eve/render.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*-) +# -*- coding: utf-8 -*- """ eve.render From 71925f1456c7d566d6c229b35f472bf22cdc0195 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 6 Jun 2017 16:40:44 +0200 Subject: [PATCH 195/821] New: JSON_REQUEST_CONTENT_TYPES This setting defaults to ['application/json']. Useful for supporting vendor-specific Content-Type headers. Responses will still carry application/json. Closes #1024. --- CHANGES | 7 ++++++- docs/config.rst | 6 ++++++ eve/default_settings.py | 1 + eve/methods/common.py | 4 ++-- eve/tests/config.py | 2 ++ eve/tests/methods/post.py | 12 ++++++++++++ 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 48dcac90f..ce6125a6d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,13 +8,18 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful + when you need support for vendor-specific json types. Please note: responses + will still carry the standard ``application/json`` type. Defaults to + ``['application/json']``. Closes #1024. - New: ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` allows custom fields in GeoJSON (Martin Fous). - New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. Closes #769 (Martin Fous). - Config options ``MONGO_AUTH_MECHANISM`` and ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. -- Change: Support for Cerberus 1.0+. Closes #776 (Dominik Kellner, Brad P. Crochet). +- Change: Support for Cerberus 1.0+. Closes #776 (Dominik Kellner, Brad P. + Crochet). - Change: Drop Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). - Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). - Dev: after the latest update (May 4th) travis-ci would not run tests on diff --git a/docs/config.rst b/docs/config.rst index dda24d0e2..068a51ab8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -487,6 +487,12 @@ uppercase. ``JSON_SORT_KEYS`` ``True`` to enable JSON key sorting, ``False`` otherwise. Defaults to ``False``. +``JSON_REQUEST_CONTENT_TYPES`` Supported JSON content types. Useful when + you need support for vendor-specific json + types. Please note: responses will still + carry the standard ``application/json`` + type. Defaults to ``['application/json']``. + ``VALIDATION_ERROR_STATUS`` The HTTP status code to use for validation errors. Defaults to ``422``. diff --git a/eve/default_settings.py b/eve/default_settings.py index 732fc638b..4e4cb68d7 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -207,6 +207,7 @@ MULTIPART_FORM_FIELDS_AS_JSON = False AUTO_COLLAPSE_MULTI_KEYS = False AUTO_CREATE_LISTS = False +JSON_REQUEST_CONTENT_TYPES = ['application/json'] SCHEMA_ENDPOINT = None diff --git a/eve/methods/common.py b/eve/methods/common.py index d816a5b6c..43006600d 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -161,8 +161,8 @@ def payload(): """ content_type = request.headers.get('Content-Type', '').split(';')[0] - if content_type == 'application/json': - return request.get_json() + if content_type in config.JSON_REQUEST_CONTENT_TYPES: + return request.get_json(force=True) elif content_type == 'application/x-www-form-urlencoded': return multidict_to_dict(request.form) if len(request.form) else \ abort(400, description='No form-urlencoded data supplied') diff --git a/eve/tests/config.py b/eve/tests/config.py index d02a41991..181269632 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -87,6 +87,8 @@ def test_default_settings(self): self.assertEqual(self.app.config['STANDARD_ERRORS'], [400, 401, 404, 405, 406, 409, 410, 412, 422, 428]) self.assertEqual(self.app.config['UPSERT_ON_PUT'], True) + self.assertEqual(self.app.config['JSON_REQUEST_CONTENT_TYPES'], + ['application/json']) def test_settings_as_dict(self): my_settings = {'API_VERSION': 'override!', 'DOMAIN': {'contacts': {}}} diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index a05fb3dc3..198e6cdb8 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -875,6 +875,18 @@ def test_post_location_header_hateoas_off(self): self.assertTrue('Location' in r.headers) self.assertTrue(self.known_resource_url in r.headers['Location']) + def test_post_custom_json_content_type(self): + data = {'ref': '1234567890123456789054321'} + r, status = self.post(self.known_resource_url, data, + content_type='application/csp-report') + self.assert400(status) + + self.app.config['JSON_REQUEST_CONTENT_TYPES'] += \ + ['application/csp-report'] + r, status = self.post(self.known_resource_url, data, + content_type='application/csp-report') + self.assert201(status) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From 066c83eb6e6c98486c386e43476ec4ac10e0b923 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Jun 2017 15:11:49 +0200 Subject: [PATCH 196/821] Fix insidious bug in tests.TestPost class --- CHANGES | 1 + eve/tests/methods/post.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ce6125a6d..c97bb56e5 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,7 @@ Version 0.8 Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. +- Dev: fix insidious bug in ``tests.methods.post.TestPost`` class. - Update: Upgrade Events dependency to v0.3. Breaking Changes diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 198e6cdb8..feb02a0ef 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -936,7 +936,9 @@ def compare_post_with_get(self, item_id, fields): else: return item[fields] - def post(self, url, data, headers=[], content_type='application/json'): + def post(self, url, data, headers=None, content_type='application/json'): + if not headers: + headers=[] headers.append(('Content-Type', content_type)) r = self.test_client.post(url, data=json.dumps(data), headers=headers) return self.parse_response(r) From d5ec45835ea596933875855f939949ab486c42e3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 15 Jun 2017 10:42:16 +0200 Subject: [PATCH 197/821] Delete unnecessary code --- CHANGES | 1 + eve/flaskapp.py | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index c97bb56e5..5c08ba992 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,7 @@ Version 0.8 Crochet). - Change: Drop Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). - Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). +- Dev: Delete and cleanup of some unnecessary code. - Dev: after the latest update (May 4th) travis-ci would not run tests on Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 2fa9deef7..dba6e4ba2 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -616,15 +616,6 @@ def _set_resource_defaults(self, resource, settings): schema = settings.setdefault('schema', {}) self.set_schema_defaults(schema, settings['id_field']) - # list of all media fields for the resource - settings['_media'] = [field for field, definition in schema.items() if - definition.get('type') == 'media'] - - if settings['_media'] and not self.media: - raise ConfigException('A media storage class of type ' - ' eve.io.media.MediaStorage but be defined ' - 'for "media" fields to be properly stored.') - self._set_resource_datasource(resource, schema, settings) def _set_resource_datasource(self, resource, schema, settings): From 4d65436d20ec41a9e94b7d4d249f2e969658056a Mon Sep 17 00:00:00 2001 From: Amedeo91 Date: Tue, 20 Jun 2017 23:35:52 +0200 Subject: [PATCH 198/821] Bulk Embedded document resolution --- dev-requirements.txt | 2 +- eve/io/mongo/mongo.py | 18 ++--- eve/methods/common.py | 184 ++++++++++++++++++++++++++++++++++-------- requirements.txt | 1 + setup.py | 1 + 5 files changed, 162 insertions(+), 44 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 0f1bef02d..6ed6c6f89 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -13,4 +13,4 @@ Sphinx==1.2.3 tox==2.4.1 wheel==0.24.0 testfixtures==4.1.2 -alabaster==0.7.10 +alabaster==0.7.10 \ No newline at end of file diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index e196b0b6e..0ad0090a2 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -178,10 +178,10 @@ def find(self, resource, req, sub_resource_lookup): """ args = dict() - if req.max_results: + if req and req.max_results: args['limit'] = req.max_results - if req.page > 1: + if req and req.page > 1: args['skip'] = (req.page - 1) * req.max_results # TODO sort syntax should probably be coherent with 'where': either @@ -194,7 +194,7 @@ def find(self, resource, req, sub_resource_lookup): client_sort = {} spec = {} - if req.sort: + if req and req.sort: try: # assume it's mongo syntax (ie. ?sort=[("name", 1)]) client_sort = ast.literal_eval(req.sort) @@ -213,7 +213,7 @@ def find(self, resource, req, sub_resource_lookup): self.app.logger.exception(e) abort(400, description=debug_error_message(str(e))) - if req.where: + if req and req.where: try: spec = self._sanitize(json.loads(req.where)) except HTTPException as e: @@ -235,13 +235,13 @@ def find(self, resource, req, sub_resource_lookup): if sub_resource_lookup: spec = self.combine_queries(spec, sub_resource_lookup) - if config.DOMAIN[resource]['soft_delete'] and not req.show_deleted: + if config.DOMAIN[resource]['soft_delete'] \ + and not (req and req.show_deleted) \ + and not self.query_contains_field(spec, config.DELETED): # Soft delete filtering applied after validate_filters call as # querying against the DELETED field must always be allowed when # soft_delete is enabled - if not self.query_contains_field(spec, config.DELETED): - spec = self.combine_queries( - spec, {config.DELETED: {"$ne": True}}) + spec = self.combine_queries(spec, {config.DELETED: {"$ne": True}}) spec = self._mongotize(spec, resource) @@ -253,7 +253,7 @@ def find(self, resource, req, sub_resource_lookup): client_projection, client_sort) - if req.if_modified_since: + if req and req.if_modified_since: spec[config.LAST_UPDATED] = \ {'$gt': req.if_modified_since} diff --git a/eve/methods/common.py b/eve/methods/common.py index 43006600d..b22d05bb8 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -10,6 +10,10 @@ :license: BSD, see LICENSE for more details. """ import base64 +try: + from collections import Counter +except: + from backport_collections import Counter import simplejson as json import time @@ -673,7 +677,7 @@ def resolve_embedded_fields(resource, req): return enabled_embedded_fields -def embedded_document(reference, data_relation, field_name): +def embedded_document(references, data_relation, field_name): """ Returns a document to be embedded by reference using data_relation taking into account document versions @@ -683,40 +687,156 @@ def embedded_document(reference, data_relation, field_name): .. versionadded:: 0.5 """ + embedded_docs = [] + + output_is_list = True + + if not isinstance(references, list): + output_is_list = False + references = [references] + # Retrieve and serialize the requested document if 'version' in data_relation and data_relation['version'] is True: - # grab the specific version - embedded_doc = get_data_version_relation_document( - data_relation, reference) - - # grab the latest version - latest_embedded_doc = get_data_version_relation_document( - data_relation, reference, latest=True) - - # make sure we got the documents - if embedded_doc is None or latest_embedded_doc is None: - # your database is not consistent!!! that is bad - # TODO: we should notify the developers with a log. - abort(404, description=debug_error_message( - "Unable to locate embedded documents for '%s'" % - field_name - )) - - build_response_document(embedded_doc, data_relation['resource'], - [], latest_embedded_doc) + # For the version flow, I keep the as-is logic (flow is too complex to make it bulk) + for reference in references: + # grab the specific version + embedded_doc = get_data_version_relation_document( + data_relation, reference) + + # grab the latest version + latest_embedded_doc = get_data_version_relation_document( + data_relation, reference, latest=True) + + # make sure we got the documents + if embedded_doc is None or latest_embedded_doc is None: + # your database is not consistent!!! that is bad + # TODO: we should notify the developers with a log. + abort(404, description=debug_error_message( + "Unable to locate embedded documents for '%s'" % + field_name + )) + + build_response_document(embedded_doc, data_relation['resource'], + [], latest_embedded_doc) + embedded_docs.append(embedded_doc) else: + id_value_to_sort, list_of_id_field_name, subresources_query = generate_query_and_sorting_criteria(data_relation, + references) + for subresource in subresources_query: + list_embedded_doc = list(app.data.find(subresource, + None, + subresources_query[subresource])) + if not list_embedded_doc: + embedded_docs.extend([None] * + len(subresources_query[subresource]["$or"])) + else: + for embedded_doc in list_embedded_doc: + resolve_media_files(embedded_doc, subresource) + embedded_docs.extend(list_embedded_doc) + + # After having retrieved my data, I have to be sure that the sorting of the + # list is the same in input as in output (this is to support embedding of + # sub-documents - only in case the storage is not done via DBref) + if embedded_docs: + embedded_docs = sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name) + + if output_is_list: + return embedded_docs + elif embedded_docs: + return embedded_docs[0] + else: + return None + + +def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): + """ Sorts the documents fetched from the database + + :param embedded_docs: the documents fetch from the database. + :param id_value_to_sort: id_value sort criteria. + :param list_of_id_field_name: list of name of fields + :return embedded_docs: the list of documents sorted as per input + """ + + id_field_name_occurrences = Counter(list_of_id_field_name) + temp_embedded_docs = [] + old_occurrence = 0 + + for id_field_name in set(list_of_id_field_name): + current_occurrence = old_occurrence + int(id_field_name_occurrences[id_field_name]) + temp_embedded_docs.extend( + sort_per_resource(embedded_docs[old_occurrence:current_occurrence], + id_value_to_sort, + id_field_name)) + old_occurrence = current_occurrence + + return temp_embedded_docs + + +def sort_per_resource(embedded_docs, id_value_to_sort, id_field_name): + """ Sorts the documents fetched from the database per single resource + + :param embedded_docs: the documents fetch from the database. + :param id_value_to_sort: id_value sort criteria. + :param list_of_id_field_name: list of name of fields + :return embedded_docs: the list of documents sorted as per input + """ + # Removing None + number_of_none = embedded_docs.count(None) + if number_of_none: + embedded_docs = [x for x in embedded_docs if x is not None] + id2dict = dict((d[id_field_name], d) for d in embedded_docs) + temporary_embedded_docs = [] + if number_of_none: + for id_value_ in id_value_to_sort: + if id_value_ in id2dict: + temporary_embedded_docs.append(id2dict[id_value_]) + else: + temporary_embedded_docs.append(None) + return embedded_docs + + +def generate_query_and_sorting_criteria(data_relation, references): + """ Generate query and sorting critiria + + :param data_relation: data relation for the resource. + :param references: DBRef or id to use to embed the document. + :returns id_value_to_sort: list of ids to use in the sort + list_of_id_field_name: list of field name (important only for DBRef) + subresources_query: the list of query to perform per resource + (in case is not DBRef, it will be only one query) + """ + query = {"$or": []} + subresources_query = {} + old_subresource = "" + id_value_to_sort = [] + # id_field name should be the same for + # all the elements in the list + list_of_id_field_name = [] + for counter, reference in enumerate(references): # if reference is DBRef take the referenced collection as subresource + # NOTE: using DBRef, I can define several resource for each link subresource = reference.collection if isinstance(reference, DBRef) \ else data_relation['resource'] - id_field = config.DOMAIN[subresource]['id_field'] - embedded_doc = app.data.find_one(subresource, None, - **{id_field: reference.id - if isinstance(reference, DBRef) - else reference}) - if embedded_doc: - resolve_media_files(embedded_doc, subresource) - - return embedded_doc + if old_subresource and old_subresource != subresource: + add_query_to_list(query, subresource, subresources_query) + # NOTE: in case it is a DBRef link, the id_field_name is always the _id + # regardless the Eve set-up + id_field_name = "_id" if isinstance(reference, DBRef) \ + else config.DOMAIN[subresource]['id_field'] + id_field_value = reference.id \ + if isinstance(reference, DBRef) else reference + query["$or"].append({id_field_name: id_field_value}) + id_value_to_sort.append(id_field_value) + list_of_id_field_name.append(id_field_name) + if counter == len(references) - 1: + add_query_to_list(query, subresource, subresources_query) + return id_value_to_sort, list_of_id_field_name, subresources_query + + +def add_query_to_list(query, subresource, subresource_query): + subresource_query.update({subresource: copy(query)}) + query.clear() + query["$or"] = [] def subdocuments(fields_chain, resource, document): @@ -788,11 +908,7 @@ def resolve_embedded_documents(document, resource, embedded_fields): for subdocument in subdocuments(fields_chain[:-1], resource, document): if last_field not in subdocument: continue - if isinstance(subdocument[last_field], list): - subdocument[last_field] = list(map(getter, - subdocument[last_field])) - else: - subdocument[last_field] = getter(subdocument[last_field]) + subdocument[last_field] = getter(subdocument[last_field]) def resolve_media_files(document, resource): diff --git a/requirements.txt b/requirements.txt index 7081c0442..8cc2b3fb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ MarkupSafe==0.23 pymongo==3.4.0 simplejson==3.8.2 Werkzeug==0.11.15 +backport_collections==0.1 diff --git a/setup.py b/setup.py index 830136e90..fe68bcabd 100755 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ 'itsdangerous>=0.24,<1.0', 'flask>=0.10.1,<=0.12', 'pymongo>=3.4', + 'backport_collections>=0.1', ] try: From 87186acf964a7889b800095631698f91bafcd526 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 21 Jun 2017 10:04:06 +0200 Subject: [PATCH 199/821] Changelog for #1031 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 5c08ba992..a05f60424 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Performance improved on retrieving a list of embedded documents. Closes + #1029 (Amedeo91). - New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful when you need support for vendor-specific json types. Please note: responses will still carry the standard ``application/json`` type. Defaults to From 4eeb9fc41a50b38ab766fe345cd041a6e1207930 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 21 Jun 2017 10:04:36 +0200 Subject: [PATCH 200/821] Amedeo91 --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d1269ac75..b4a313731 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` - Alexander Hendorf +- Amedeo91 - Andreas Røssland - Andrés Martano - Antonio Lourenco From 890290030a935479fbaf992e135c974426ab5a26 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Jun 2017 11:23:04 +0200 Subject: [PATCH 201/821] typo Reported by Vasilis Lolis via email. --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 068a51ab8..820e71db7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -152,7 +152,7 @@ uppercase. ``PAGINATION_DEFAULT`` Default value for QUERY_MAX_RESULTS. Defaults to 25. -``OPTMIMIZE_PAGINATION_FOR_SPEED`` Set this to ``True`` to improve pagination +``OPTIMIZE_PAGINATION_FOR_SPEED`` Set this to ``True`` to improve pagination performance. When optimization is active no count operation, which can be slow on large collections, is performed on the database. From 32a6adcac2b28b5013bf71299b4df893921840b4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Jun 2017 11:23:36 +0200 Subject: [PATCH 202/821] Vasilis Lolis --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b4a313731..a780bb54f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -148,6 +148,7 @@ Patches and Contributions - Tim Jacobi - Tomasz Jezierski - Valerie Coffman +- Vasilis Lolis - Wael M. Nasreddine - Wei Guan - Xavi Cubillas From 9cf3db1e6c755c6c6358b20082e360bb735b71fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 6 Jul 2017 15:38:24 +0200 Subject: [PATCH 203/821] MONGO_DBNAME can be now used along with MONGO_URI Closes #1037 --- CHANGES | 8 ++++++-- eve/io/mongo/flask_pymongo.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index a05f60424..27465b79b 100644 --- a/CHANGES +++ b/CHANGES @@ -8,8 +8,12 @@ Development Version 0.8 ~~~~~~~~~~~ -- Performance improved on retrieving a list of embedded documents. Closes - #1029 (Amedeo91). +- New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. + Previously, if ``MONGO_URI`` was missing the database name, an exception + would be rised. + Closes #1037. +- Performance improved on retrieving a list of embedded documents. Closes #1029 + (Amedeo91). - New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful when you need support for vendor-specific json types. Please note: responses will still carry the standard ``application/json`` type. Defaults to diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index da1037bf9..6b4d077e9 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -67,7 +67,7 @@ def config_to_kwargs(mapping): mongo_settings = uri_parser.parse_uri(host) dbname = mongo_settings.get('database') if not dbname: - raise ValueError('MongoDB URI does not contain database name') + dbname = app.config[key('DBNAME')] else: dbname = app.config[key('DBNAME')] host = app.config[key('HOST')] From 86e351fd0c2481e28540020f06b87dc111ec6773 Mon Sep 17 00:00:00 2001 From: Amedeo91 Date: Tue, 20 Jun 2017 20:55:06 +0200 Subject: [PATCH 204/821] Bulk delete --- docs/features.rst | 56 +++++++++++++++++++++++++++++++++++-- eve/io/base.py | 13 +++++---- eve/io/mongo/mongo.py | 15 +++++----- eve/methods/common.py | 9 ++++-- eve/methods/delete.py | 47 +++++++++++++++++++++---------- eve/tests/__init__.py | 18 +++++++++++- eve/tests/methods/delete.py | 43 ++++++++++++++++++++++------ eve/tests/methods/get.py | 2 +- 8 files changed, 162 insertions(+), 41 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 9323c4a17..b97e11fd9 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -174,6 +174,40 @@ a simple resource endpoint the document lookup would happen on a single field: invoices/ +Endpoints that supports sub-resources will have a specific deletion behaviour +for the DELETION operations +Indeed, a DELETE to the following endpoint: + +:: + + people/51f63e0838345b6dcd7eabff/invoices + +would cause the delete all the documents that can be founded with the follow query: + +:: + + {'contact_id': '51f63e0838345b6dcd7eabff'} + + +Therefore, for sub-resource end-point, not all the collection will be deleted, but +only the documents that can be find with the same end-point. + +Another example, if a DELETE to the following item endpoint: + +:: + + people/51f63e0838345b6dcd7eabff/invoices/1 + +would cause the delete all the documents that can be founded with the follow query: + +:: + + {'contact_id': '51f63e0838345b6dcd7eabff', "": 1} + +This behaviour is to be able to support tree structure where the id of the resource alone +is not a primary key by itself. + + .. _custom_item_endpoints: Customizable, multiple item endpoints @@ -1000,6 +1034,10 @@ individually configured at the resource level using the domain configuration ``soft_delete`` setting. See :ref:`global` and :ref:`domain` for more information on enabling and configuring soft delete. +In case the soft deletion is enabled, the callback on_delete_resource_originals +and on_delete_resource_originals_ will receive as originals the +soft_deleted documents as well with the not deleted ones. + Behavior ~~~~~~~~ With soft delete enabled, DELETE requests to individual items and resources @@ -1270,10 +1308,16 @@ Let's see an overview of what events are available: | | | || ``def event(item)`` | | +--------+------+-------------------------------------------------+ | |Resource|Before|| ``on_delete_resource`` | -| | | || ``def event(resource_name, item)`` | +| | | || ``def event(resource_name)`` | | | | +-------------------------------------------------+ | | | || ``on_delete_resource_`` | -| | | || ``def event(item)`` | +| | | || ``def event()`` | +| | | +-------------------------------------------------+ +| | | || ``on_delete_resource_originals`` | +| | | || ``def event(resource_name, originals, lookup)``| +| | | +-------------------------------------------------+ +| | | ||``on_delete_resource_originals_``| +| | | || ``def event(originals, lookup)`` | | | +------+-------------------------------------------------+ | | |After || ``on_deleted_resource`` | | | | || ``def event(resource_name, item)`` | @@ -1441,6 +1485,8 @@ These are the delete events with their method signature: - ``on_deleted_item_(item)`` - ``on_delete_resource(resource_name)`` - ``on_delete_resource_()`` +- ``on_delete_resource_originals(originals, lookup)`` +- ``on_delete_resource_originals_(originals, lookup)`` - ``on_deleted_resource(resource_name)`` - ``on_deleted_resource_()`` @@ -1471,6 +1517,12 @@ notified of such a disastrous occurrence by hooking a callback function to the ``on_delete_resource(resource_name)`` or ``on_delete_resource_()`` hooks. +- ``on_delete_resource_originals`` for any resource hit by the request after having retrieved the originals documents. +- ``on_delete_resource_originals_`` for the specific `` resource endpoint + hit by the DELETE after having retrieved the original document. NOTE: those two event are useful in order to + perform some business logic before the actual remove operation given the look up and the list of originals + + .. admonition:: Please note diff --git a/eve/io/base.py b/eve/io/base.py index 401f78141..37c42c381 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -175,13 +175,14 @@ def find_one(self, resource, req, **lookup): """ raise NotImplementedError - def find_one_raw(self, resource, _id): + + def find_one_raw(self, resource, **lookup): """ Retrieves a single, raw document. No projections or datasource - filters are being applied here. Just looking up the document by unique - id. + filters are being applied here. Just looking up the document using the same lookup. - :param resource: resource name. - :param id: unique id. + :param + resource: resource name. + :param ** lookup: lookup query. .. versionadded:: 0.4 """ @@ -249,7 +250,7 @@ def replace(self, resource, id_, document, original): """ raise NotImplementedError - def remove(self, resource, lookup={}): + def remove(self, resource, lookup): """ Removes a document/row or an entire set of documents/rows from a database collection/table. diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 0ad0090a2..69f6ad472 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -311,15 +311,14 @@ def find_one(self, resource, req, **lookup): filter_ = self.combine_queries( filter_, {config.DELETED: {"$ne": True}}) - document = self.pymongo(resource).db[datasource] \ - .find_one(filter_, projection) - return document + return self.pymongo(resource).db[datasource] \ + .find_one(filter_, projection) - def find_one_raw(self, resource, _id): + def find_one_raw(self, resource, **lookup): """ Retrieves a single raw document. :param resource: resource name. - :param id: unique id. + :param **lookup: lookup query. .. versionchanged:: 0.6 Support for multiple databases. @@ -327,12 +326,14 @@ def find_one_raw(self, resource, _id): .. versionadded:: 0.4 """ id_field = config.DOMAIN[resource]['id_field'] + _id = lookup.get(id_field) datasource, filter_, _, _ = self._datasource_ex(resource, {id_field: _id}, None) - document = self.pymongo(resource).db[datasource].find_one(_id) - return document + lookup = self._mongotize(lookup, resource) + + return self.pymongo(resource).db[datasource].find_one(lookup) def find_list_of_ids(self, resource, ids, client_projection=None): """ Retrieves a list of documents from the collection given diff --git a/eve/methods/common.py b/eve/methods/common.py index b22d05bb8..53cdd91f9 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -37,7 +37,7 @@ from werkzeug.datastructures import MultiDict, CombinedMultiDict -def get_document(resource, concurrency_check, **lookup): +def get_document(resource, concurrency_check, original=None, **lookup): """ Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client request references the current representation of the document before @@ -47,6 +47,7 @@ def get_document(resource, concurrency_check, **lookup): :param resource: the name of the resource to which the document belongs to. :param concurrency_check: boolean check for concurrency control + :param original: in case the document was already retrieved before :param **lookup: document lookup query .. versionchanged:: 0.6 @@ -69,7 +70,11 @@ def get_document(resource, concurrency_check, **lookup): # callers must handle soft deleted documents req.show_deleted = True - document = app.data.find_one(resource, req, **lookup) + if original: + document = original + else: + document = app.data.find_one(resource, req, **lookup) + if document: e_if_m = config.ENFORCE_IF_MATCH if_m = config.IF_MATCH diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 526202197..6b9bb46e2 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -38,13 +38,14 @@ def deleteitem(resource, **lookup): def deleteitem_internal( - resource, concurrency_check=False, suppress_callbacks=False, **lookup): + resource, concurrency_check=False, suppress_callbacks=False, original=None, **lookup): """ Intended for internal delete calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Deletes a resource item. :param resource: name of the resource to which the item(s) belong. :param concurrency_check: concurrency check switch (bool) + :param original: original document if already fetched from the database :param **lookup: item lookup query. .. versionchanged:: 0.6 @@ -83,7 +84,7 @@ def deleteitem_internal( """ resource_def = config.DOMAIN[resource] soft_delete_enabled = resource_def['soft_delete'] - original = get_document(resource, concurrency_check, **lookup) + original = get_document(resource, concurrency_check, original, **lookup) if not original or (soft_delete_enabled and original.get(config.DELETED) is True): abort(404) @@ -137,8 +138,7 @@ def deleteitem_internal( # get_document() call since it also deals with etag matching, which # is still needed. Also, this lookup should never fail. # TODO not happy with this hack. Not at all. Is there a better way? - original = app.data.find_one_raw( - resource, original[resource_def['id_field']]) + original = app.data.find_one_raw(resource, **lookup) for field in media_fields: if field in original: @@ -150,7 +150,7 @@ def deleteitem_internal( app.media.delete(original[field], resource) id = original[resource_def['id_field']] - app.data.remove(resource, {resource_def['id_field']: id}) + app.data.remove(resource, lookup) # TODO: should attempt to delete version collection even if setting is # off @@ -193,20 +193,39 @@ def delete(resource, **lookup): .. versionadded:: 0.0.2 """ - getattr(app, "on_delete_resource")(resource) - getattr(app, "on_delete_resource_%s" % resource)() resource_def = config.DOMAIN[resource] + getattr(app, "on_delete_resource")(resource) + getattr(app, "on_delete_resource_%s" % resource)() + default_request = ParsedRequest() + if resource_def['soft_delete']: + # get_document should always fetch soft deleted documents from the db + # callers must handle soft deleted documents + default_request.show_deleted = True + originals = list(app.data.find(resource, default_request, lookup)) + if not originals: + abort(404) + # I add new callback as I want the framework to be retro-compatible + getattr(app, "on_delete_resource_originals")(resource, + originals, + lookup) + getattr(app, "on_delete_resource_originals_%s" % resource)(originals, + lookup) + id_field = resource_def['id_field'] if resource_def['soft_delete']: - # Soft delete all items not already marked deleted - # (by default, data.find doesn't return soft deleted items) - default_request = ParsedRequest() - cursor = app.data.find(resource, default_request, lookup) - for document in list(cursor): - document_id = document[resource_def['id_field']] + # I need to check that I have at least some documents not soft_deleted + # Otherwise, I should abort 404 + # I skip all the soft_deleted documents + originals = [x for x in originals if x.get(config.DELETED) is not True] + if not originals: + # Nothing to be deleted + abort(404) + for document in originals: + lookup[id_field] = document[id_field] deleteitem_internal(resource, concurrency_check=False, - suppress_callbacks=True, _id=document_id) + suppress_callbacks=True, + original=document, **lookup) else: # TODO if the resource schema includes media files, these won't be # deleted by use of this global method (it should be disabled). Media diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index a170dd2ee..857c84f63 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -426,6 +426,14 @@ def setUp(self, url_converters=None): self.epoch = date_to_str(datetime(1970, 1, 1)) + self.products = 'products' + self.products_url = ('/%s' % + self.domain[self.products]['url']) + + self.child_products = 'child_products' + self.child_products_url = ('/%s' % + self.domain[self.child_products]['url']) + def response_item(self, response, i=0): if self.app.config['HATEOAS']: return response['_items'][i] @@ -539,6 +547,13 @@ def random_internal_transactions(self, num): transactions.append(transaction) return transactions + def generate_products(self): + products = self.random_products(10) + skus = [product['sku'] for product in products] + for counter, sku in enumerate(skus[5:], 0): + products[counter]['parent_product'] = sku + return products + def bulk_insert(self): _db = self.connection[MONGO_DBNAME] _db.contacts.insert(self.random_contacts(self.known_resource_count)) @@ -546,5 +561,6 @@ def bulk_insert(self): _db.payments.insert(self.random_payments(10)) _db.invoices.insert(self.random_invoices(1)) _db.internal_transactions.insert(self.random_internal_transactions(4)) - _db.products.insert(self.random_products(2)) + products = self.generate_products() + _db.products.insert(products) self.connection.close() diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index cb45464ca..10c6303ed 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -21,6 +21,27 @@ def test_unknown_resource(self): _, status = self.delete(url) self.assert404(status) + def test_bulk_delete_id_field(self): + etag_check = self.app.config["IF_MATCH"] + self.app.config["IF_MATCH"] = False + products, _ = self.get(self.products) + list_products_skus = [product["parent_product"] for product in products["_items"] + if "parent_product" in product] + # Deletion of all the product in the first cart + url = self.child_products_url.replace('', list_products_skus[0]) + _, status = self.delete(url) + self.assert204(status) + _, status = self.get(url) + self.assert404(status) + products_url = '%s/%s' % (self.products, list_products_skus[0]) + _, status = self.delete(products_url) + self.assert204(status) + _, status = self.get(products_url) + self.assert404(status) + _, status = self.get(self.products) + self.assert200(status) + self.app.config["IF_MATCH"] = etag_check + def test_delete_from_resource_endpoint(self): r, status = self.delete(self.known_resource_url) self.assert204(status) @@ -745,17 +766,23 @@ def test_on_delete_resource(self): self.delete_resource() self.assertEqual(('contacts',), devent.called) - def test_on_delete_resource_contacts(self): - devent = DummyEvent(self.before_delete) - self.app.on_delete_resource_contacts += devent + def test_on_delete_resource(self): + devent1 = DummyEvent(self.before_delete) + self.app.on_delete_resource += devent1 + devent2 = DummyEvent(self.before_delete) + self.app.on_delete_resource_originals += devent2 self.delete_resource() - self.assertEqual(tuple(), devent.called) + self.assertEqual(('contacts',), devent1.called) + self.assertFalse(devent2.called is None) - def test_on_deleted_resource(self): - devent = DummyEvent(self.after_delete) - self.app.on_deleted_resource += devent + def test_on_delete_resource_contacts(self): + devent1 = DummyEvent(self.before_delete) + self.app.on_delete_resource_contacts += devent1 + devent2 = DummyEvent(self.before_delete) + self.app.on_delete_resource_originals_contacts += devent2 self.delete_resource() - self.assertEqual(('contacts',), devent.called) + self.assertEqual(tuple(), devent1.called) + self.assertFalse(devent2.called is None) def test_on_deleted_resource_contacts(self): devent = DummyEvent(self.after_delete) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index bc88b82b5..2729babde 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1113,7 +1113,7 @@ def test_get_custom_idfield(self): self.assertHomeLink(links) self.assertResourceLink(links, 'products') items = response['_items'] - self.assertEqual(2, len(items)) + self.assertEqual(10, len(items)) for item in items: self.assertItem(item, 'products') From a151c80dd66a1cb2630000e8d0459e4e85437c15 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 15 Jul 2017 11:31:09 +0200 Subject: [PATCH 205/821] documentation improvements and fixes --- docs/features.rst | 192 +++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 94 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index b97e11fd9..34bc5bf26 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -174,38 +174,40 @@ a simple resource endpoint the document lookup would happen on a single field: invoices/ -Endpoints that supports sub-resources will have a specific deletion behaviour -for the DELETION operations -Indeed, a DELETE to the following endpoint: + +Endpoints that supports sub-resources will have a specific behavior on +``DELETE`` operations. A ``DELETE`` to the following endpoint: :: people/51f63e0838345b6dcd7eabff/invoices -would cause the delete all the documents that can be founded with the follow query: +would cause the deletion of all the documents that match follow query: :: {'contact_id': '51f63e0838345b6dcd7eabff'} -Therefore, for sub-resource end-point, not all the collection will be deleted, but -only the documents that can be find with the same end-point. +Therefore, for sub-resource endpoints, only the documents satisfying the +endpoint semantic will be deleted. This differs from the standard behavior, +whereas a delete operation on a collection enpoint will cause the deletion of +all the documents in the collection. -Another example, if a DELETE to the following item endpoint: +Another example. A ``DELETE`` to the following item endpoint: :: people/51f63e0838345b6dcd7eabff/invoices/1 -would cause the delete all the documents that can be founded with the follow query: +would cause the deletion all the documents matched by the follow query: :: {'contact_id': '51f63e0838345b6dcd7eabff', "": 1} -This behaviour is to be able to support tree structure where the id of the resource alone -is not a primary key by itself. +This behaviour enables support for typical tree structures, where the id of the +resource alone is not necessarily a primary key by itself. .. _custom_item_endpoints: @@ -1034,9 +1036,11 @@ individually configured at the resource level using the domain configuration ``soft_delete`` setting. See :ref:`global` and :ref:`domain` for more information on enabling and configuring soft delete. -In case the soft deletion is enabled, the callback on_delete_resource_originals -and on_delete_resource_originals_ will receive as originals the -soft_deleted documents as well with the not deleted ones. +When soft deletion is enabled, callbacks attached to +``on_delete_resource_originals`` and +``on_delete_resource_originals_`` events will receive both +deleted and not deleted documents via the ``originals`` argument (see +:ref:`eventhooks`). Behavior ~~~~~~~~ @@ -1244,87 +1248,87 @@ both. And for each action two events will be fired: Let's see an overview of what events are available: -+-------+--------+------+-------------------------------------------------+ -|Action |What |When |Event name / method signature | -+=======+========+======+=================================================+ -|Fetch |Resource|After || ``on_fetched_resource`` | -| | | || ``def event(resource_name, response)`` | -| | | +-------------------------------------------------+ -| | | || ``on_fetched_resource_`` | -| | | || ``def event(response)`` | -| +--------+------+-------------------------------------------------+ -| |Item |After || ``on_fetched_item`` | -| | | || ``def event(resource_name, response)`` | -| | | +-------------------------------------------------+ -| | | || ``on_fetched_item_`` | -| | | || ``def event(response)`` | -+-------+--------+------+-------------------------------------------------+ -|Insert |Items |Before|| ``on_insert`` | -| | | || ``def event(resource_name, items)`` | -| | | +-------------------------------------------------+ -| | | || ``on_insert_`` | -| | | || ``def event(items)`` | -| | +------+-------------------------------------------------+ -| | |After || ``on_inserted`` | -| | | || ``def event(resource_name, items)`` | -| | | +-------------------------------------------------+ -| | | || ``on_inserted_`` | -| | | || ``def event(items)`` | -+-------+--------+------+-------------------------------------------------+ -|Replace|Item |Before|| ``on_replace`` | -| | | || ``def event(resource_name, item, original)`` | -| | | +-------------------------------------------------+ -| | | || ``on_replace_`` | -| | | || ``def event(item, original)`` | -| | +------+-------------------------------------------------+ -| | |After || ``on_replaced`` | -| | | || ``def event(resource_name, item, original)`` | -| | | +-------------------------------------------------+ -| | | || ``on_replaced_`` | -| | | || ``def event(item, original)`` | -+-------+--------+------+-------------------------------------------------+ -|Update |Item |Before|| ``on_update`` | -| | | || ``def event(resource_name, updates, original)``| -| | | +-------------------------------------------------+ -| | | || ``on_update_`` | -| | | || ``def event(updates, original)`` | -| | +------+-------------------------------------------------+ -| | |After || ``on_updated`` | -| | | || ``def event(resource_name, updates, original)``| -| | | +-------------------------------------------------+ -| | | || ``on_updated_`` | -| | | || ``def event(updates, original)`` | -+-------+--------+------+-------------------------------------------------+ -|Delete |Item |Before|| ``on_delete_item`` | -| | | || ``def event(resource_name, item)`` | -| | | +-------------------------------------------------+ -| | | || ``on_delete_item_`` | -| | | || ``def event(item)`` | -| | +------+-------------------------------------------------+ -| | |After || ``on_deleted_item`` | -| | | || ``def event(resource_name, item)`` | -| | | +-------------------------------------------------+ -| | | || ``on_deleted_item_`` | -| | | || ``def event(item)`` | -| +--------+------+-------------------------------------------------+ -| |Resource|Before|| ``on_delete_resource`` | -| | | || ``def event(resource_name)`` | -| | | +-------------------------------------------------+ -| | | || ``on_delete_resource_`` | -| | | || ``def event()`` | -| | | +-------------------------------------------------+ -| | | || ``on_delete_resource_originals`` | -| | | || ``def event(resource_name, originals, lookup)``| -| | | +-------------------------------------------------+ -| | | ||``on_delete_resource_originals_``| -| | | || ``def event(originals, lookup)`` | -| | +------+-------------------------------------------------+ -| | |After || ``on_deleted_resource`` | -| | | || ``def event(resource_name, item)`` | -| | | +-------------------------------------------------+ -| | | || ``on_deleted_resource_`` | -| | | || ``def event(item)`` | -+-------+--------+------+-------------------------------------------------+ ++-------+--------+------+--------------------------------------------------+ +|Action |What |When |Event name / method signature | ++=======+========+======+==================================================+ +|Fetch |Resource|After || ``on_fetched_resource`` | +| | | || ``def event(resource_name, response)`` | +| | | +--------------------------------------------------+ +| | | || ``on_fetched_resource_`` | +| | | || ``def event(response)`` | +| +--------+------+--------------------------------------------------+ +| |Item |After || ``on_fetched_item`` | +| | | || ``def event(resource_name, response)`` | +| | | +--------------------------------------------------+ +| | | || ``on_fetched_item_`` | +| | | || ``def event(response)`` | ++-------+--------+------+--------------------------------------------------+ +|Insert |Items |Before|| ``on_insert`` | +| | | || ``def event(resource_name, items)`` | +| | | +--------------------------------------------------+ +| | | || ``on_insert_`` | +| | | || ``def event(items)`` | +| | +------+--------------------------------------------------+ +| | |After || ``on_inserted`` | +| | | || ``def event(resource_name, items)`` | +| | | +--------------------------------------------------+ +| | | || ``on_inserted_`` | +| | | || ``def event(items)`` | ++-------+--------+------+--------------------------------------------------+ +|Replace|Item |Before|| ``on_replace`` | +| | | || ``def event(resource_name, item, original)`` | +| | | +--------------------------------------------------+ +| | | || ``on_replace_`` | +| | | || ``def event(item, original)`` | +| | +------+--------------------------------------------------+ +| | |After || ``on_replaced`` | +| | | || ``def event(resource_name, item, original)`` | +| | | +--------------------------------------------------+ +| | | || ``on_replaced_`` | +| | | || ``def event(item, original)`` | ++-------+--------+------+--------------------------------------------------+ +|Update |Item |Before|| ``on_update`` | +| | | || ``def event(resource_name, updates, original)`` | +| | | +--------------------------------------------------+ +| | | || ``on_update_`` | +| | | || ``def event(updates, original)`` | +| | +------+--------------------------------------------------+ +| | |After || ``on_updated`` | +| | | || ``def event(resource_name, updates, original)`` | +| | | +--------------------------------------------------+ +| | | || ``on_updated_`` | +| | | || ``def event(updates, original)`` | ++-------+--------+------+--------------------------------------------------+ +|Delete |Item |Before|| ``on_delete_item`` | +| | | || ``def event(resource_name, item)`` | +| | | +--------------------------------------------------+ +| | | || ``on_delete_item_`` | +| | | || ``def event(item)`` | +| | +------+--------------------------------------------------+ +| | |After || ``on_deleted_item`` | +| | | || ``def event(resource_name, item)`` | +| | | +--------------------------------------------------+ +| | | || ``on_deleted_item_`` | +| | | || ``def event(item)`` | +| +--------+------+--------------------------------------------------+ +| |Resource|Before|| ``on_delete_resource`` | +| | | || ``def event(resource_name)`` | +| | | +--------------------------------------------------+ +| | | || ``on_delete_resource_`` | +| | | || ``def event()`` | +| | | +--------------------------------------------------+ +| | | || ``on_delete_resource_originals`` | +| | | || ``def event(resource_name, originals, lookup)`` | +| | | +--------------------------------------------------+ +| | | || ``on_delete_resource_originals_``| +| | | || ``def event(originals, lookup)`` | +| | +------+--------------------------------------------------+ +| | |After || ``on_deleted_resource`` | +| | | || ``def event(resource_name, item)`` | +| | | +--------------------------------------------------+ +| | | || ``on_deleted_resource_`` | +| | | || ``def event(item)`` | ++-------+--------+------+--------------------------------------------------+ From e2ebf838e092fad2ec20e7673dd776d6caed0c79 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 15 Jul 2017 11:40:14 +0200 Subject: [PATCH 206/821] pep/flake, and remove duplicate test --- eve/io/base.py | 7 +++---- eve/methods/delete.py | 4 ++-- eve/tests/methods/delete.py | 13 ++++--------- eve/tests/methods/post.py | 2 +- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/eve/io/base.py b/eve/io/base.py index 37c42c381..6df507c2b 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -175,13 +175,12 @@ def find_one(self, resource, req, **lookup): """ raise NotImplementedError - def find_one_raw(self, resource, **lookup): """ Retrieves a single, raw document. No projections or datasource - filters are being applied here. Just looking up the document using the same lookup. + filters are being applied here. Just looking up the document using the + same lookup. - :param - resource: resource name. + :param resource: resource name. :param ** lookup: lookup query. .. versionadded:: 0.4 diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 6b9bb46e2..6fff91e3c 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -37,8 +37,8 @@ def deleteitem(resource, **lookup): return deleteitem_internal(resource, concurrency_check=True, **lookup) -def deleteitem_internal( - resource, concurrency_check=False, suppress_callbacks=False, original=None, **lookup): +def deleteitem_internal(resource, concurrency_check=False, + suppress_callbacks=False, original=None, **lookup): """ Intended for internal delete calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Deletes a resource item. diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 10c6303ed..78417d9a9 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -25,10 +25,11 @@ def test_bulk_delete_id_field(self): etag_check = self.app.config["IF_MATCH"] self.app.config["IF_MATCH"] = False products, _ = self.get(self.products) - list_products_skus = [product["parent_product"] for product in products["_items"] - if "parent_product" in product] + list_products_skus = [product["parent_product"] for product in + products["_items"] if "parent_product" in product] # Deletion of all the product in the first cart - url = self.child_products_url.replace('', list_products_skus[0]) + url = self.child_products_url.replace( + '', list_products_skus[0]) _, status = self.delete(url) self.assert204(status) _, status = self.get(url) @@ -760,12 +761,6 @@ def test_on_post_DELETE_resource_for_resource(self): self.delete_resource() self.assertFalse(devent.called is None) - def test_on_delete_resource(self): - devent = DummyEvent(self.before_delete) - self.app.on_delete_resource += devent - self.delete_resource() - self.assertEqual(('contacts',), devent.called) - def test_on_delete_resource(self): devent1 = DummyEvent(self.before_delete) self.app.on_delete_resource += devent1 diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index feb02a0ef..b93078705 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -938,7 +938,7 @@ def compare_post_with_get(self, item_id, fields): def post(self, url, data, headers=None, content_type='application/json'): if not headers: - headers=[] + headers = [] headers.append(('Content-Type', content_type)) r = self.test_client.post(url, data=json.dumps(data), headers=headers) return self.parse_response(r) From 611499f5bad2e7fd64c33707eded6e9d5d576bcf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 15 Jul 2017 11:50:35 +0200 Subject: [PATCH 207/821] Changelog for #1030 --- CHANGES | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 27465b79b..78711b29d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,12 +8,14 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: ``on_delete_resource_originals`` fired when soft deletion occurs + (Amedeo Bussi). - New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. Previously, if ``MONGO_URI`` was missing the database name, an exception would be rised. Closes #1037. - Performance improved on retrieving a list of embedded documents. Closes #1029 - (Amedeo91). + (Amedeo Bussi). - New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful when you need support for vendor-specific json types. Please note: responses will still carry the standard ``application/json`` type. Defaults to @@ -27,6 +29,8 @@ Version 0.8 - Change: Support for Cerberus 1.0+. Closes #776 (Dominik Kellner, Brad P. Crochet). - Change: Drop Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). +- Change: ``DELETE`` on sub-resource endpoints will only delete the documents. + that match the endpoint semantics. Addresses #1010 (Amedeo Bussi). - Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). - Dev: Delete and cleanup of some unnecessary code. - Dev: after the latest update (May 4th) travis-ci would not run tests on @@ -53,6 +57,10 @@ Breaking Changes ``MONGO_CONNECT_TIMEOUT_MS``, ``MONGO_REPLICA_SET``, ``MONGO_READ_PREFERENCE`` removed. Use ``MONGO_OPTIONS`` or ``MONGO_URI`` instead. +- Be aware that ``DELETE`` on sub-resource endpoint will now only delete the + documents matching endpoint semantics. A delete operation on + ``people/51f63e0838345b6dcd7eabff/invoices`` will delete all documents + matching the followig query: ``{'contact_id': '51f63e0838345b6dcd7eabff'}`` Stable ------ From 7303ad891b4ebf797168054962a309ab033d2aea Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 15 Jul 2017 11:52:30 +0200 Subject: [PATCH 208/821] Amedeo Bussi --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index a780bb54f..837771202 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,7 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` - Alexander Hendorf -- Amedeo91 +- Amedeo Bussi - Andreas Røssland - Andrés Martano - Antonio Lourenco From 75989836be00835511e77437381f0d2b2f33a241 Mon Sep 17 00:00:00 2001 From: "Zhang, Ruiqi" Date: Fri, 28 Jul 2017 13:59:21 +0800 Subject: [PATCH 209/821] Fix a serialization bug that randomly skips fields if "x_of" is encountered --- AUTHORS | 1 + eve/methods/common.py | 33 +++++++++++++++---------------- eve/tests/methods/common.py | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/AUTHORS b/AUTHORS index 837771202..be0b2bfec 100644 --- a/AUTHORS +++ b/AUTHORS @@ -125,6 +125,7 @@ Patches and Contributions - Petr Jašek - Prayag Verma - Ralph Smith +- Raychee - Robert Wlodarczyk - Roberto 'Kalamun' Pasini - Rodrigo Rodriguez diff --git a/eve/methods/common.py b/eve/methods/common.py index 53cdd91f9..6ea458635 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -387,15 +387,16 @@ def serialize(document, resource=None, schema=None, fields=None): if field in schema: field_schema = schema[field] field_type = field_schema.get('type') - if field_type is None: - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - schema = {field: optschema} - serialize(document, schema=schema) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - schema = {field: {'type': opttype}} - serialize(document, schema=schema) + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + optschema = dict(field_schema, **optschema) + optschema.pop(x_of, None) + serialize(document, schema={field: optschema}) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + optschema = dict(field_schema, type=opttype) + optschema.pop(x_of_type, None) + serialize(document, schema={field: optschema}) if config.AUTO_CREATE_LISTS and field_type == 'list': # Convert single values to lists if not isinstance(document[field], list): @@ -432,16 +433,14 @@ def serialize(document, resource=None, schema=None, fields=None): # a list of items determined by *of rules for x_of in ['allof', 'anyof', 'oneof', 'noneof']: for optschema in field_schema.get(x_of, []): - schema = {field: { - 'type': field_type, - 'schema': optschema}} - serialize(document, schema=schema) + serialize(document, + schema={field: {'type': field_type, + 'schema': optschema}}) x_of_type = '{0}_type'.format(x_of) for opttype in field_schema.get(x_of_type, []): - schema = {field: { - 'type': field_type, - 'schema': {'type': opttype}}} - serialize(document, schema=schema) + serialize(document, + schema={field: {'type': field_type, + 'schema': {'type': opttype}}}) else: # a list of one type, arbitrary length field_type = field_schema.get('type') diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 88ac4a63b..b92ae516d 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -11,6 +11,12 @@ from eve.tests.test_settings import MONGO_DBNAME from eve.utils import config +try: + from collections import OrderedDict # noqa +except ImportError: + # Python 2.6 needs this back-port + from ordereddict import OrderedDict + class TestSerializer(TestBase): def test_serialize_subdocument(self): @@ -329,6 +335,39 @@ def test_serialize_inside_x_of_rules(self): serialized = serialize(doc, schema=schema) self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + def test_serialize_alongside_x_of_rules(self): + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + schema = OrderedDict([ + ('x_of-field', { + x_of: [ + {'type': 'objectid'}, + {'required': True} + ] + }), + ('oid-field', {'type': 'objectid'}) + ]) + doc = OrderedDict([('x_of-field', '50656e4538345b39dd0414f0'), ('oid-field', '50656e4538345b39dd0414f0')]) + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + self.assertTrue(isinstance(serialized['oid-field'], ObjectId)) + + def test_serialize_list_alongside_x_of_rules(self): + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + schema = { + 'x_of-field': { + "type": "list", + x_of: [ + {"schema": {'type': 'objectid'}}, + {"schema": {'type': 'datetime'}} + ] + } + } + doc = {'x_of-field': ['50656e4538345b39dd0414f0']} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertTrue(isinstance(serialized['x_of-field'][0], ObjectId)) + def test_serialize_inside_nested_x_of_rules(self): schema = { 'nested-x_of-field': { From 8b94bbca3dc18ea343a3e7bd139b19e0a60f4f2f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 9 Aug 2017 11:28:53 +0200 Subject: [PATCH 210/821] Changelog update for #1042 --- CHANGES | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 78711b29d..ff5e50d8a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,12 +8,13 @@ Development Version 0.8 ~~~~~~~~~~~ -- New: ``on_delete_resource_originals`` fired when soft deletion occurs - (Amedeo Bussi). +- Fix: serialization bug that randomly skips fields if "x_of" is encountered + (Raychee). +- New: ``on_delete_resource_originals`` fired when soft deletion occurs (Amedeo + Bussi). - New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. Previously, if ``MONGO_URI`` was missing the database name, an exception - would be rised. - Closes #1037. + would be rised. Closes #1037. - Performance improved on retrieving a list of embedded documents. Closes #1029 (Amedeo Bussi). - New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful From 4dfbc5b91145505eeac1bb497aec89e328c0a2c7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 9 Aug 2017 11:31:43 +0200 Subject: [PATCH 211/821] Changelog: add reference to proper PR --- CHANGES | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index ff5e50d8a..109bb6fa0 100644 --- a/CHANGES +++ b/CHANGES @@ -8,8 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ -- Fix: serialization bug that randomly skips fields if "x_of" is encountered - (Raychee). +- Fix: serialization bug that randomly skips fields if "x_of" is encountered. + See PR #1042 for details (Raychee). - New: ``on_delete_resource_originals`` fired when soft deletion occurs (Amedeo Bussi). - New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. From 3bf193039a11ff13597ffb72192958562513f4f8 Mon Sep 17 00:00:00 2001 From: Amedeo91 Date: Sat, 19 Aug 2017 20:22:18 +0200 Subject: [PATCH 212/821] Support Decimal type MongoDB --- .travis.yml | 15 ++++++++++++--- CHANGES | 2 ++ docs/config.rst | 1 + docs/validation.rst | 3 ++- eve/io/mongo/mongo.py | 7 +++++++ eve/io/mongo/validation.py | 6 +++++- eve/tests/io/mongo.py | 16 +++++++++++++++- eve/tests/methods/common.py | 12 +++++++++--- eve/tests/methods/post.py | 19 ++++++++++++++++++- eve/tests/test_settings.py | 1 + requirements.txt | 2 +- setup.py | 2 +- 12 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebaaf4b4d..391e31a31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,19 @@ python: - pypy install: travis_retry pip install tox-travis services: - - mongodb + #- mongodb - redis-server before_script: + # work-around to make travis-ci working with mongod 3.4 + # https://github.com/travis-ci/travis-ci/issues/3694 + # https://github.com/travis-ci/apt-package-whitelist/issues/516 + - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.7.tgz -O /tmp/mongodb.tgz + - tar -xvf /tmp/mongodb.tgz + - mkdir /tmp/data + - ${PWD}/mongodb-linux-x86_64-3.4.7/bin/mongod --dbpath /tmp/data --bind_ip 127.0.0.1 --noauth &> /dev/null & + - until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done + - sleep 15 # timer is needed in order to get mongo to properly initialize on travis-ci # See https://github.com/travis-ci/travis-ci/issues/1967#issuecomment-42008605 - - sleep 15 - - mongo eve_test --eval 'db.addUser("test_user", "test_pw");' + - "${PWD}/mongodb-linux-x86_64-3.4.7/bin/mongo eve_test --eval 'db.createUser({\"user\": \"test_user\", \"pwd\": \"test_pw\", \"roles\": [\"readWrite\", \"dbAdmin\"]},{\"w\": \"majority\" , \"wtimeout\": 5000 })'" + #- mongo eve_test --eval 'db.addUser("test_user", "test_pw");' \ No newline at end of file diff --git a/CHANGES b/CHANGES index 109bb6fa0..7d67159e4 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Supporting new mongodb decimal data-type (bson.decimal128.Decimal128) +- Updating to pymongo 3.5 - Fix: serialization bug that randomly skips fields if "x_of" is encountered. See PR #1042 for details (Raychee). - New: ``on_delete_resource_originals`` fired when soft deletion occurs (Amedeo diff --git a/docs/config.rst b/docs/config.rst index 820e71db7..d588b4470 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1176,6 +1176,7 @@ defining the field validation rules. Allowed validation rules are: - ``polygon`` - ``multipolygon`` - ``geometrycollection`` + - ``decimal`` See :ref:`GeoJSON ` for more informations geo fields. diff --git a/docs/validation.rst b/docs/validation.rst index 7cf6c3a1e..c0dd9968d 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -47,7 +47,8 @@ Extending Data Validation Data validation is based on the Cerberus_ validation system and it is therefore extensible. As a matter of fact, Eve's MongoDB data-layer itself extends Cerberus validation, implementing the ``unique`` and ``data_relation`` -constraints and the ``ObjectId`` data type on top of the standard rules. +constraints, the ``ObjectId`` data type and the ``decimal128`` on top of +the standard rules. .. _custom_validation_rules: diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 69f6ad472..f2cceafe1 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -22,6 +22,8 @@ from .flask_pymongo import PyMongo from pymongo import WriteConcern from werkzeug.exceptions import HTTPException +import decimal +from bson import decimal128 from eve.auth import resource_auth from eve.io.base import DataLayer, ConnectionException, BaseJSONEncoder @@ -53,6 +55,8 @@ def default(self, obj): if obj.database: retval['$db'] = obj.database return retval + if isinstance(obj, decimal128.Decimal128): + return str(obj) # delegate rendering to base class method return super(MongoJSONEncoder, self).default(obj) @@ -85,6 +89,9 @@ class Mongo(DataLayer): 'dbref': lambda value: DBRef(value['$col'], value['$id'], value['$db'] if '$db' in value else None) if value is not None else None, + 'decimal': lambda value: + decimal128.Decimal128(decimal.Decimal(str(value))) + if value is not None else None, } # JSON serializer is a class attribute. Allows extensions to replace it diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 512205c5a..312295a66 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -11,7 +11,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -from bson import ObjectId +from bson import ObjectId, decimal128 from bson.dbref import DBRef from flask import current_app as app from werkzeug.datastructures import FileStorage @@ -167,6 +167,10 @@ def _validate_type_objectid(self, value): if isinstance(value, ObjectId): return True + def _validate_type_decimal(self, value): + if isinstance(value, decimal128.Decimal128): + return True + def _validate_type_dbref(self, value): if isinstance(value, DBRef): return True diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index a15c980fd..83a91448b 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -2,7 +2,7 @@ from datetime import datetime import simplejson as json -from bson import ObjectId +from bson import ObjectId, decimal128 from bson.dbref import DBRef from cerberus import SchemaError from unittest import TestCase @@ -94,6 +94,20 @@ def test_unique_success(self): app_context running here """ pass + def test_decimal_fail(self): + schema = {'decimal': {'type': 'decimal'}} + doc = {'decimal': 'not_a_decimal'} + v = Validator(schema, None) + self.assertFalse(v.validate(doc)) + self.assertTrue('decimal' in v.errors) + self.assertTrue('decimal' in v.errors['decimal']) + + def test_decimal_success(self): + schema = {'decimal': {'type': 'decimal'}} + doc = {'decimal': decimal128.Decimal128('123.123')} + v = Validator(schema, None) + self.assertTrue(v.validate(doc)) + def test_objectid_fail(self): schema = {'id': {'type': 'objectid'}} doc = {'id': 'not_an_object_id'} diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index b92ae516d..df080aecc 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -2,7 +2,7 @@ from datetime import datetime import simplejson as json -from bson import ObjectId +from bson import ObjectId, decimal128 from bson.dbref import DBRef from eve.methods.common import serialize, normalize_dotted_fields @@ -44,7 +44,9 @@ def test_mongo_serializes(self): 'dict_valueschema': { 'valueschema': {'type': 'objectid'} }, - 'refobj': {'type': 'dbref'} + 'refobj': {'type': 'dbref'}, + 'decobjstring': {'type': 'decimal'}, + 'decobjnumber': {'type': 'decimal'} } with self.app.app_context(): # Success @@ -61,7 +63,9 @@ def test_mongo_serializes(self): 'refobj': { '$id': '50656e4538345b39dd0414f0', '$col': 'SomeCollection' - } + }, + 'decobjstring': "200.0", + 'decobjnumber': 200.0 }, schema=schema ) @@ -74,6 +78,8 @@ def test_mongo_serializes(self): self.assertTrue(isinstance(ks['foo1'], ObjectId)) self.assertTrue(isinstance(ks['foo2'], ObjectId)) self.assertTrue(isinstance(res['refobj'], DBRef)) + self.assertTrue(isinstance(res['decobjstring'], decimal128.Decimal128)) + self.assertTrue(isinstance(res['decobjnumber'], decimal128.Decimal128)) def test_non_blocking_on_simple_field_serialization_exception(self): schema = { diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index b93078705..dd937c38d 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1,5 +1,5 @@ from base64 import b64decode -from bson import ObjectId +from bson import ObjectId, decimal128 import simplejson as json @@ -10,6 +10,7 @@ from eve import STATUS_OK, LAST_UPDATED, DATE_CREATED, ISSUES, STATUS, ETAG from eve.methods.post import post from eve.methods.post import post_internal +from eve.utils import str_type from io import BytesIO @@ -343,6 +344,22 @@ def test_post_auto_create_lists(self): r, status = self.parse_response(resp) self.assert201(status) + def test_post_decimal_number_success(self): + data = {"decimal_number": 100} + r, status = self.post('/invoices/', data=data) + self.assert201(status) + self.assertPostResponse(r) + id_field = self.domain['invoices']['id_field'] + unique_id = r[id_field] + r, status = self.get('invoices/%s' % unique_id) + self.assert200(status) + assert isinstance(r["decimal_number"], str_type) + + def test_post_decimal_number_fail(self): + data = {"decimal_number": "100.0.0"} + r, status = self.post('/invoices/', data=data) + self.assert422(status) + def test_post_referential_integrity(self): data = {"person": self.unknown_item_id} r, status = self.post('/invoices/', data=data) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 9dd2363bc..818045c0a 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -184,6 +184,7 @@ 'type': 'dbref', 'data_relation': {'resource': 'contacts'} }, + 'decimal_number': {'type': 'decimal'}, } } diff --git a/requirements.txt b/requirements.txt index 8cc2b3fb9..e8c6dcc15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ Flask==0.12 itsdangerous==0.24 Jinja2==2.9.4 MarkupSafe==0.23 -pymongo==3.4.0 +pymongo==3.5.0 simplejson==3.8.2 Werkzeug==0.11.15 backport_collections==0.1 diff --git a/setup.py b/setup.py index fe68bcabd..b77c576c8 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', 'flask>=0.10.1,<=0.12', - 'pymongo>=3.4', + 'pymongo>=3.5', 'backport_collections>=0.1', ] From 46af495c2f316c16cd080ad9fec2ccf423f6528c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 29 Aug 2017 08:44:27 +0200 Subject: [PATCH 213/821] Minor changelog fixes for #1048 --- CHANGES | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 7d67159e4..6329e98a3 100644 --- a/CHANGES +++ b/CHANGES @@ -8,8 +8,9 @@ Development Version 0.8 ~~~~~~~~~~~ -- Supporting new mongodb decimal data-type (bson.decimal128.Decimal128) -- Updating to pymongo 3.5 +- New: support fpr MongoDB decimal type ``bson.decimal128.Decimal128`` (Amedeo + Bussi). +- Update: upgrade PyMongo dependency to v3.5 (Amedeo Bussi). - Fix: serialization bug that randomly skips fields if "x_of" is encountered. See PR #1042 for details (Raychee). - New: ``on_delete_resource_originals`` fired when soft deletion occurs (Amedeo From fa8da196d1034f01401915277fa7dafd1080bcd5 Mon Sep 17 00:00:00 2001 From: Carl George Date: Tue, 10 Oct 2017 11:19:14 -0500 Subject: [PATCH 214/821] use OrderedDict from backport_collections It is unnecessary to require the ordereddict module because the backport_collections module already has the OrderedDict class. This change removes the ordereddict dependency, and also ensures that backport_collections is only required when needed. --- AUTHORS | 1 + eve/render.py | 2 +- eve/tests/methods/common.py | 2 +- py26-requirements.txt | 2 +- requirements.txt | 1 - setup.py | 7 +++---- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index be0b2bfec..03b99986c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,6 +22,7 @@ Patches and Contributions - Brad P. Crochet - Brian Mego - Bryan Cattle +- Carl George - Carles Bruguera - Christian Henke - Christoph Witzany diff --git a/eve/render.py b/eve/render.py index aea46317e..0ddcdde79 100644 --- a/eve/render.py +++ b/eve/render.py @@ -25,7 +25,7 @@ from collections import OrderedDict # noqa except ImportError: # Python 2.6 needs this back-port - from ordereddict import OrderedDict + from backport_collections import OrderedDict # mapping between supported mime types and render functions. _MIME_TYPES = [ diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index df080aecc..b8a3400db 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -15,7 +15,7 @@ from collections import OrderedDict # noqa except ImportError: # Python 2.6 needs this back-port - from ordereddict import OrderedDict + from backport_collections import OrderedDict class TestSerializer(TestBase): diff --git a/py26-requirements.txt b/py26-requirements.txt index 4f81f46e5..8851829aa 100644 --- a/py26-requirements.txt +++ b/py26-requirements.txt @@ -1,2 +1,2 @@ -r requirements.txt -ordereddict +backport_collections==0.1 diff --git a/requirements.txt b/requirements.txt index e8c6dcc15..bcad8e0d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,3 @@ MarkupSafe==0.23 pymongo==3.5.0 simplejson==3.8.2 Werkzeug==0.11.15 -backport_collections==0.1 diff --git a/setup.py b/setup.py index b77c576c8..1f8c54395 100755 --- a/setup.py +++ b/setup.py @@ -15,14 +15,13 @@ 'itsdangerous>=0.24,<1.0', 'flask>=0.10.1,<=0.12', 'pymongo>=3.5', - 'backport_collections>=0.1', ] try: - from collections import OrderedDict # noqa + from collections import Counter, OrderedDict # noqa except ImportError: - # Python 2.6 needs this back-port - install_requires.append('ordereddict') + # Python 2.6 + install_requires.append('backport_collections') setup( From 9fb1a668ebaaba8705fb17b458f9d90207d18f14 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Oct 2017 10:02:37 +0200 Subject: [PATCH 215/821] Changelog for #1070 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 6329e98a3..9d978305a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: Removed OrderedDict dependency; use ``OrderedDict`` from + ``backport_collections`` instead (Carl George). - New: support fpr MongoDB decimal type ``bson.decimal128.Decimal128`` (Amedeo Bussi). - Update: upgrade PyMongo dependency to v3.5 (Amedeo Bussi). From 6c15df694cc0f52ae51d918f7c8eabcda343784c Mon Sep 17 00:00:00 2001 From: Serge Kir Date: Mon, 4 Sep 2017 00:56:54 +0300 Subject: [PATCH 216/821] Handling lists as part of aggregation stage with parameters --- eve/methods/get.py | 25 ++++++++++++++++----- eve/tests/methods/get.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index b47b872b4..213eb31ad 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -121,12 +121,27 @@ def _perform_aggregation(resource, pipeline, options): # TODO experiment with cursor.batch_size as alternative pagination # implementation - def parse_aggregation_stage(d, key, value): - for st_key, st_value in d.items(): - if isinstance(st_value, dict): + def parse_aggregation_stage(st_item, key, value): + def parse_again(st_value, key, value): + """ + If stage value is list or dict then parse recursively + """ + if isinstance(st_value, (list, dict)): parse_aggregation_stage(st_value, key, value) - if key == st_value: - d[st_key] = value + + if isinstance(st_item, dict): + for st_key, st_value in st_item.items(): + if key == st_value: + st_item[st_key] = value + else: + parse_again(st_value, key, value) + + elif isinstance(st_item, list): + for st_i, st_value in enumerate(st_item): + if key == st_value: + st_item[st_i] = value + else: + parse_again(st_value, key, value) response = {} documents = [] diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2729babde..2fa26ae29 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1243,6 +1243,53 @@ def test_get_aggregation_parsing(self): docs = response['_items'] self.assertEqual(len(docs), 4) + def test_get_aggregation_with_lists(self): + _db = self.connection[MONGO_DBNAME] + _db.aggregate_test.insert_many( + [ + {"x": 1, "tags": ["a", "b", "c"]}, + {"x": 2, "tags": ["a"]}, + {"x": 3, "tags": ["a", "b"]}, + {"x": [4], "tags": []}, + ] + ) + + self.app.register_resource( + 'aggregate_test', { + 'datasource': { + 'aggregation': { + 'pipeline': [ + { + "$match": { + "$or": [ + {"tags": "$match_tags"}, + {"x": ["$x"]} + ] + } + } + ] + } + } + } + ) + + response, status = self.get( + 'aggregate_test?aggregate={"$match_tags": "a"}') + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 3) + + response, status = self.get( + 'aggregate_test?aggregate={"$match_tags": ["a", "b"]}') + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 1) + + response, status = self.get('aggregate_test?aggregate={"$x": 4}') + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 1) + def test_get_aggregation_pagination(self): _db = self.connection[MONGO_DBNAME] From bf59b1cde9f3a3cc14dc46273a42c2196970a235 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Nov 2017 11:18:50 +0300 Subject: [PATCH 217/821] Changelog for #1058 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 9d978305a..219bac8ca 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: Aggregation query parameter does not replace keys in the lists. Closes + #1025 (Serge Kir). - Fix: Removed OrderedDict dependency; use ``OrderedDict`` from ``backport_collections`` instead (Carl George). - New: support fpr MongoDB decimal type ``bson.decimal128.Decimal128`` (Amedeo From 6cbfd43d4dd24989c50dd2d5ff0af8cd71b85b0f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Nov 2017 11:19:58 +0300 Subject: [PATCH 218/821] Serge Kir --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 03b99986c..473f53b62 100644 --- a/AUTHORS +++ b/AUTHORS @@ -139,6 +139,7 @@ Patches and Contributions - Samuli Tuomola - Sebastien Estienne - Sebastián Magrí +- Serge Kir - Simon Schönfeld - Sobolev Nikita - Stanislav Filin From 46fd06ae0fb97dd03a2dc95c55f4a575798294ba Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 30 Aug 2017 10:04:23 -0700 Subject: [PATCH 219/821] Add support to the bitwise query operator. Bitwise query operator e.g., `$bitsAllClear` can be useful for query over number. However, `eve` doesn't support it yet. This PR add the supports of bitwise query operator. --- eve/io/mongo/mongo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index f2cceafe1..f9e86c967 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -105,7 +105,8 @@ class Mongo(DataLayer): ['$options', '$search', '$language'] + ['$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + - ['$all', '$elemMatch', '$size'] + ['$all', '$elemMatch', '$size'] + + ['$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'] ) def init_app(self, app): From 7acb1932d5eeac6034122638fafb31663c71c4b4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Nov 2017 09:13:26 +0100 Subject: [PATCH 220/821] Add test for bitwise query operator feature. --- eve/tests/methods/get.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2fa26ae29..561bd9613 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1361,6 +1361,30 @@ def test_get_aggregation_pagination(self): items = response['_items'] self.assertEqual(len(items), num) + def test_get_query_bitwise_query_operators(self): + del(self.domain['contacts']['schema']['ref']['required']) + response, status = self.delete(self.known_resource_url) + self.assert204(status) + + data = {'prog': 20} # 00010100 + response, status = self.post(self.known_resource_url, data=data) + self.assert201(status) + + where = '?where={"prog": {"$bitsAllClear": [1, 5]}}' + response, status = self.get(self.known_resource, where) + self.assert200(status) + items = response['_items'] + self.assertEqual(1, len(items)) + + response, status = self.delete(self.known_resource_url) + self.assert204(status) + + where = '?where={"prog": {"$bitsAllClear": [2, 5]}}' + response, status = self.get(self.known_resource, where) + self.assert200(status) + items = response['_items'] + self.assertEqual(0, len(items)) + def assertGet(self, response, status, resource=None): self.assert200(status) @@ -1696,7 +1720,7 @@ def test_getitem_lookup_field_as_string(self): # treated as a string when 'query_objectid_as_string' is set to True. # See PR #552. data = {'id': '507c7f79bcf86cd7994f6c0e', 'name': 'john'} - response, status = self.post('ids', data=data) + response, status = self.post(self.known_resource_url, data=data) self.assert201(status) response, status = self.get('ids', item='507c7f79bcf86cd7994f6c0e') self.assert200(status) From ee5f4f4fbcbda315701f92e52a2f54a743c2d1e5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Nov 2017 09:16:53 +0100 Subject: [PATCH 221/821] Changelog update for #1055 --- CHANGES | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 219bac8ca..9c042119d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,9 @@ Development Version 0.8 ~~~~~~~~~~~ -- Fix: Aggregation query parameter does not replace keys in the lists. Closes +- New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, + ``bitsAllSet``, ``bitsAnyClear``, ``bitsAnySet``. Closes 1053 (Qiang Zhang). +- Fix: Aggregation query parameter does not replace keys in the lists. Closes #1025 (Serge Kir). - Fix: Removed OrderedDict dependency; use ``OrderedDict`` from ``backport_collections`` instead (Carl George). From d793d5ce7c708f25f6d02094c2a7976e15bb18ed Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Nov 2017 09:17:45 +0100 Subject: [PATCH 222/821] Qiang Zhang --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 473f53b62..be7d5cd47 100644 --- a/AUTHORS +++ b/AUTHORS @@ -125,6 +125,7 @@ Patches and Contributions - Peter Darrow - Petr Jašek - Prayag Verma +- Qiang Zhang - Ralph Smith - Raychee - Robert Wlodarczyk From 37c83068c0cd80e667296e44292a4fee6677fc69 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 7 Nov 2017 10:02:44 +0100 Subject: [PATCH 223/821] Fix test which was broken by previous PR --- eve/tests/methods/get.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 561bd9613..8f58afec9 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1376,9 +1376,6 @@ def test_get_query_bitwise_query_operators(self): items = response['_items'] self.assertEqual(1, len(items)) - response, status = self.delete(self.known_resource_url) - self.assert204(status) - where = '?where={"prog": {"$bitsAllClear": [2, 5]}}' response, status = self.get(self.known_resource, where) self.assert200(status) @@ -1720,7 +1717,7 @@ def test_getitem_lookup_field_as_string(self): # treated as a string when 'query_objectid_as_string' is set to True. # See PR #552. data = {'id': '507c7f79bcf86cd7994f6c0e', 'name': 'john'} - response, status = self.post(self.known_resource_url, data=data) + response, status = self.post('ids', data=data) self.assert201(status) response, status = self.get('ids', item='507c7f79bcf86cd7994f6c0e') self.assert200(status) From e7a54065d204ad780c086b9956c5e0cbc0f81bbc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 9 Nov 2017 17:45:29 +0100 Subject: [PATCH 224/821] New: media endpoint secured by default auth class. Closes #1083. --- CHANGES | 2 ++ eve/auth.py | 3 +-- eve/endpoints.py | 1 + eve/tests/auth.py | 33 ++++++++++++++++++++++++++++++++- eve/tests/io/__init__.py | 3 +++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 9c042119d..fc93e1e80 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: when the media endpoint is enabled, the default authentication class + will be used to secure it. Closes #1083. - New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, ``bitsAllSet``, ``bitsAnyClear``, ``bitsAnySet``. Closes 1053 (Qiang Zhang). - Fix: Aggregation query parameter does not replace keys in the lists. Closes diff --git a/eve/auth.py b/eve/auth.py index 2c2590d9d..6cc063188 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -34,7 +34,6 @@ def fdec(f): @wraps(f) def decorated(*args, **kwargs): if endpoint_class == 'resource' or endpoint_class == 'item': - # find resource name in f's args if args: resource_name = args[0] elif kwargs.get('resource'): @@ -64,7 +63,7 @@ def decorated(*args, **kwargs): roles += resource['allowed_item_write_roles'] auth = resource_auth(resource_name) else: - # home + # home or media endpoints resource_name = resource = None public = app.config['PUBLIC_METHODS'] + ['OPTIONS'] roles = list(app.config['ALLOWED_ROLES']) diff --git a/eve/endpoints.py b/eve/endpoints.py index 905a9ccb9..fc598bfd3 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -168,6 +168,7 @@ def _resource(): return request.endpoint.split('|')[0] +@requires_auth('media') def media_endpoint(_id): """ This endpoint is active when RETURN_MEDIA_AS_URL is True. It retrieves a media file and streams it to the client. diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 17b17741e..ffbc02b6f 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- +import json + from bson import ObjectId import eve -import json from eve import Eve from eve.auth import BasicAuth, TokenAuth, HMACAuth from eve.tests import TestBase from eve.tests.test_settings import MONGO_DBNAME +from io import BytesIO class ValidBasicAuth(BasicAuth): @@ -57,6 +59,8 @@ def setUp(self): self.content_type] self.invalid_auth = [('Authorization', 'Basic IDontThinkSo'), self.content_type] + self.valid_media_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0'), + ('Content-Type', 'multipart/form-data')] self.setUpRoles() self.app.set_defaults() @@ -117,6 +121,25 @@ def test_authorized_item_access(self): r = self.test_client.delete(self.item_id_url, headers=self.valid_auth) self.assert428(r.status_code) + def test_authorized_media_access(self): + self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False + self.app.config['RETURN_MEDIA_AS_URL'] = True + self.app.config['BANDWIDTH_SAVER'] = False + self.app._init_media_endpoint() + + clean = b'my new file contents' + test_field, test_value = 'ref', "9234567890123456789054321" + data = {'media': (BytesIO(clean), 'test.txt'), test_field: test_value} + r, s = self.parse_response(self.test_client.post( + self.known_resource_url, data=data, headers=self.valid_media_auth)) + self.assert201(s) + + file_url = r['media'] + r = self.test_client.get(file_url, headers=self.invalid_auth) + self.assert401(r.status_code) + r = self.test_client.get(file_url, headers=self.valid_auth) + self.assert200(r.status_code) + def test_authorized_schema_access(self): self.app.config['SCHEMA_ENDPOINT'] = 'schema' self.app._init_schema_endpoint() @@ -271,6 +294,8 @@ def setUp(self): self.test_client = self.app.test_client() self.valid_auth = [('Authorization', 'Basic dGVzdF90b2tlbjo='), self.content_type] + self.valid_media_auth = [('Authorization', 'Basic dGVzdF90b2tlbjo='), + ('Content-Type', 'multipart/form-data')] self.setUpRoles() def test_custom_auth(self): @@ -282,6 +307,8 @@ def setUp(self): super(TestBearerTokenAuth, self).setUp() self.valid_auth = [('Authorization', 'Token test_token'), self.content_type] + self.valid_media_auth = [('Authorization', 'Token test_token'), + ('Content-Type', 'multipart/form-data')] def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadTokenAuth) @@ -296,6 +323,8 @@ def setUp(self): super(TestCustomTokenAuth, self).setUp() self.valid_auth = [('Authorization', 'Token test_token'), self.content_type] + self.valid_media_auth = [('Authorization', 'Token test_token'), + ('Content-Type', 'multipart/form-data')] def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadTokenAuth) @@ -312,6 +341,8 @@ def setUp(self): self.test_client = self.app.test_client() self.valid_auth = [('Authorization', 'admin:secret'), self.content_type] + self.valid_media_auth = [('Authorization', 'admin:secret'), + ('Content-Type', 'multipart/form-data')] self.setUpRoles() def test_custom_auth(self): diff --git a/eve/tests/io/__init__.py b/eve/tests/io/__init__.py index 40a96afc6..c516042f2 100644 --- a/eve/tests/io/__init__.py +++ b/eve/tests/io/__init__.py @@ -1 +1,4 @@ # -*- coding: utf-8 -*- +# import hack so modules importing this package can still import BytesIO from +# standard library's io module +from io import BytesIO # noqa From 4ccf8aba7e4e534ef30adcaf57f256085bf07dbc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 21 Nov 2017 09:56:17 +0100 Subject: [PATCH 225/821] Make sure non-dict schema definitions are correctly handled This is needed since Cerberus 1.1, which adds support for schema registries. When using a schema registry, a field definition can actually be a simply string: {'name': 'unique_string'} --- eve/flaskapp.py | 69 +++++++++++++++++++++++++++---------------------- eve/utils.py | 2 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index dba6e4ba2..ce4a149fe 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -439,31 +439,32 @@ def validate_field_name(field): '(they will be handled automatically).' % (', '.join(offenders), resource)) - for field, ruleset in schema.items(): - validate_field_name(field) - if 'dict' in ruleset.get('type', ''): - for field in ruleset.get('schema', {}).keys(): - validate_field_name(field) - - # check data_relation rules - if 'data_relation' in ruleset: - if 'resource' not in ruleset['data_relation']: - raise SchemaException("'resource' key is mandatory for " - "the 'data_relation' rule in " - "'%s: %s'" % (resource, field)) - if ruleset['data_relation'].get('embeddable', False): - - # special care for data_relations with a version - value_field = ruleset['data_relation']['field'] - if ruleset['data_relation'].get('version', False): - if 'schema' not in ruleset or \ - value_field not in ruleset['schema'] or \ - 'type' not in ruleset['schema'][value_field]: - raise SchemaException( - "Must defined type for '%s' in schema when " - "declaring an embedded data_relation with" - " version." % value_field - ) + if isinstance(schema, dict): + for field, ruleset in schema.items(): + validate_field_name(field) + if isinstance(ruleset, dict) and 'dict' in ruleset.get('type', ''): + for field in ruleset.get('schema', {}).keys(): + validate_field_name(field) + + # check data_relation rules + if 'data_relation' in ruleset: + if 'resource' not in ruleset['data_relation']: + raise SchemaException("'resource' key is mandatory for " + "the 'data_relation' rule in " + "'%s: %s'" % (resource, field)) + if ruleset['data_relation'].get('embeddable', False): + + # special care for data_relations with a version + value_field = ruleset['data_relation']['field'] + if ruleset['data_relation'].get('version', False): + if 'schema' not in ruleset or \ + value_field not in ruleset['schema'] or \ + 'type' not in ruleset['schema'][value_field]: + raise SchemaException( + "Must defined type for '%s' in schema when " + "declaring an embedded data_relation with" + " version." % value_field + ) # TODO are there other mandatory settings? Validate them here @@ -688,11 +689,16 @@ def _set_resource_projection(self, ds, schema, settings): ds['projection'][self.config['DELETED']] = 1 # list of all media fields for the resource - settings['_media'] = [field for field, definition in schema.items() if - definition.get('type') == 'media' or - (definition.get('type') == 'list' and - definition.get('schema', {}).get('type') == - 'media')] + if isinstance(schema, dict): + settings['_media'] = [field for field, definition in schema.items() if + isinstance(definition, dict) and + (definition.get('type') == 'media' or + (definition.get('type') == 'list' and + definition.get('schema', {}).get('type') == + 'media'))] + else: + settings['_media'] = [] + if settings['_media'] and not self.media: raise ConfigException('A media storage class of type ' @@ -721,7 +727,8 @@ def set_schema_defaults(self, schema, id_field): # DuplicateKeyConflict in the mongo layer. This also # avoids a performance hit (with 'unique' rule set, we would # end up with an extra db loopback on every insert). - schema.setdefault(id_field, {'type': 'objectid'}) + if isinstance(schema, dict): + schema.setdefault(id_field, {'type': 'objectid'}) # set default 'field' value for all 'data_relation' rulesets, however # nested diff --git a/eve/utils.py b/eve/utils.py index e78c916be..12a337554 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -347,7 +347,7 @@ def extract_key_values(key, d): if key in d: yield d[key] for k in d: - if isinstance(d[k], dict): + if isinstance(d, dict) and isinstance(d[k], dict): for j in extract_key_values(key, d[k]): yield j From 01b01fea2de8ad2557e7264ac739148da2e15f5a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 21 Nov 2017 10:32:38 +0100 Subject: [PATCH 226/821] Re-add flake8 to test runs --- eve/flaskapp.py | 67 +++++++++++++++++++------------------ eve/io/mongo/mongo.py | 2 +- eve/methods/common.py | 50 ++++++++++++++++----------- eve/tests/methods/common.py | 12 ++++--- eve/tests/methods/delete.py | 3 +- eve/tests/methods/post.py | 2 +- tox.ini | 5 +-- 7 files changed, 79 insertions(+), 62 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index ce4a149fe..7cc70e667 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -439,32 +439,34 @@ def validate_field_name(field): '(they will be handled automatically).' % (', '.join(offenders), resource)) - if isinstance(schema, dict): - for field, ruleset in schema.items(): - validate_field_name(field) - if isinstance(ruleset, dict) and 'dict' in ruleset.get('type', ''): - for field in ruleset.get('schema', {}).keys(): - validate_field_name(field) - - # check data_relation rules - if 'data_relation' in ruleset: - if 'resource' not in ruleset['data_relation']: - raise SchemaException("'resource' key is mandatory for " - "the 'data_relation' rule in " - "'%s: %s'" % (resource, field)) - if ruleset['data_relation'].get('embeddable', False): - - # special care for data_relations with a version - value_field = ruleset['data_relation']['field'] - if ruleset['data_relation'].get('version', False): - if 'schema' not in ruleset or \ - value_field not in ruleset['schema'] or \ - 'type' not in ruleset['schema'][value_field]: - raise SchemaException( - "Must defined type for '%s' in schema when " - "declaring an embedded data_relation with" - " version." % value_field - ) + if not isinstance(schema, dict): + return + + for field, ruleset in schema.items(): + validate_field_name(field) + if isinstance(ruleset, dict) and 'dict' in ruleset.get('type', ''): + for field in ruleset.get('schema', {}).keys(): + validate_field_name(field) + + # check data_relation rules + if 'data_relation' in ruleset: + if 'resource' not in ruleset['data_relation']: + raise SchemaException("'resource' key is mandatory for " + "the 'data_relation' rule in " + "'%s: %s'" % (resource, field)) + if ruleset['data_relation'].get('embeddable', False): + + # special care for data_relations with a version + value_field = ruleset['data_relation']['field'] + if ruleset['data_relation'].get('version', False): + if 'schema' not in ruleset or \ + value_field not in ruleset['schema'] or \ + 'type' not in ruleset['schema'][value_field]: + raise SchemaException( + "Must defined type for '%s' in schema when " + "declaring an embedded data_relation with" + " version." % value_field + ) # TODO are there other mandatory settings? Validate them here @@ -690,16 +692,15 @@ def _set_resource_projection(self, ds, schema, settings): # list of all media fields for the resource if isinstance(schema, dict): - settings['_media'] = [field for field, definition in schema.items() if - isinstance(definition, dict) and - (definition.get('type') == 'media' or - (definition.get('type') == 'list' and - definition.get('schema', {}).get('type') == - 'media'))] + settings['_media'] = [field for field, definition in schema.items() + if isinstance(definition, dict) and + (definition.get('type') == 'media' or + (definition.get('type') == 'list' and + definition.get('schema', {}).get('type') == + 'media'))] else: settings['_media'] = [] - if settings['_media'] and not self.media: raise ConfigException('A media storage class of type ' ' eve.io.media.MediaStorage must be defined ' diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index f9e86c967..a0e8b8be5 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -105,7 +105,7 @@ class Mongo(DataLayer): ['$options', '$search', '$language'] + ['$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + - ['$all', '$elemMatch', '$size'] + + ['$all', '$elemMatch', '$size'] + ['$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'] ) diff --git a/eve/methods/common.py b/eve/methods/common.py index 6ea458635..fc7729ddc 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -434,13 +434,16 @@ def serialize(document, resource=None, schema=None, fields=None): for x_of in ['allof', 'anyof', 'oneof', 'noneof']: for optschema in field_schema.get(x_of, []): serialize(document, - schema={field: {'type': field_type, - 'schema': optschema}}) + schema={ + field: {'type': field_type, + 'schema': optschema}}) x_of_type = '{0}_type'.format(x_of) for opttype in field_schema.get(x_of_type, []): - serialize(document, - schema={field: {'type': field_type, - 'schema': {'type': opttype}}}) + serialize( + document, + schema={field: {'type': field_type, + 'schema': {'type': + opttype}}}) else: # a list of one type, arbitrary length field_type = field_schema.get('type') @@ -701,7 +704,8 @@ def embedded_document(references, data_relation, field_name): # Retrieve and serialize the requested document if 'version' in data_relation and data_relation['version'] is True: - # For the version flow, I keep the as-is logic (flow is too complex to make it bulk) + # For the version flow, I keep the as-is logic (flow is too complex to + # make it bulk) for reference in references: # grab the specific version embedded_doc = get_data_version_relation_document( @@ -724,25 +728,28 @@ def embedded_document(references, data_relation, field_name): [], latest_embedded_doc) embedded_docs.append(embedded_doc) else: - id_value_to_sort, list_of_id_field_name, subresources_query = generate_query_and_sorting_criteria(data_relation, - references) + id_value_to_sort, list_of_id_field_name, subresources_query = \ + generate_query_and_sorting_criteria(data_relation, references) for subresource in subresources_query: - list_embedded_doc = list(app.data.find(subresource, - None, - subresources_query[subresource])) + list_embedded_doc = list( + app.data.find(subresource, None, + subresources_query[subresource])) + if not list_embedded_doc: - embedded_docs.extend([None] * - len(subresources_query[subresource]["$or"])) + embedded_docs.extend( + [None] * len(subresources_query[subresource]["$or"])) else: for embedded_doc in list_embedded_doc: resolve_media_files(embedded_doc, subresource) embedded_docs.extend(list_embedded_doc) - # After having retrieved my data, I have to be sure that the sorting of the - # list is the same in input as in output (this is to support embedding of - # sub-documents - only in case the storage is not done via DBref) + # After having retrieved my data, I have to be sure that the sorting of + # the list is the same in input as in output (this is to support + # embedding of sub-documents - only in case the storage is not done via + # DBref) if embedded_docs: - embedded_docs = sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name) + embedded_docs = sort_db_response(embedded_docs, id_value_to_sort, + list_of_id_field_name) if output_is_list: return embedded_docs @@ -766,7 +773,8 @@ def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): old_occurrence = 0 for id_field_name in set(list_of_id_field_name): - current_occurrence = old_occurrence + int(id_field_name_occurrences[id_field_name]) + current_occurrence = old_occurrence + int(id_field_name_occurrences[ + id_field_name]) temp_embedded_docs.extend( sort_per_resource(embedded_docs[old_occurrence:current_occurrence], id_value_to_sort, @@ -805,9 +813,11 @@ def generate_query_and_sorting_criteria(data_relation, references): :param data_relation: data relation for the resource. :param references: DBRef or id to use to embed the document. :returns id_value_to_sort: list of ids to use in the sort - list_of_id_field_name: list of field name (important only for DBRef) + list_of_id_field_name: list of field name (important only for + DBRef) subresources_query: the list of query to perform per resource - (in case is not DBRef, it will be only one query) + (in case is not DBRef, it will be only one + query) """ query = {"$or": []} subresources_query = {} diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index b8a3400db..16497d528 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -78,8 +78,10 @@ def test_mongo_serializes(self): self.assertTrue(isinstance(ks['foo1'], ObjectId)) self.assertTrue(isinstance(ks['foo2'], ObjectId)) self.assertTrue(isinstance(res['refobj'], DBRef)) - self.assertTrue(isinstance(res['decobjstring'], decimal128.Decimal128)) - self.assertTrue(isinstance(res['decobjnumber'], decimal128.Decimal128)) + self.assertTrue(isinstance(res['decobjstring'], + decimal128.Decimal128)) + self.assertTrue(isinstance(res['decobjnumber'], + decimal128.Decimal128)) def test_non_blocking_on_simple_field_serialization_exception(self): schema = { @@ -352,7 +354,8 @@ def test_serialize_alongside_x_of_rules(self): }), ('oid-field', {'type': 'objectid'}) ]) - doc = OrderedDict([('x_of-field', '50656e4538345b39dd0414f0'), ('oid-field', '50656e4538345b39dd0414f0')]) + doc = OrderedDict([('x_of-field', '50656e4538345b39dd0414f0'), + ('oid-field', '50656e4538345b39dd0414f0')]) with self.app.app_context(): serialized = serialize(doc, schema=schema) self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) @@ -372,7 +375,8 @@ def test_serialize_list_alongside_x_of_rules(self): doc = {'x_of-field': ['50656e4538345b39dd0414f0']} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['x_of-field'][0], ObjectId)) + self.assertTrue(isinstance(serialized['x_of-field'][0], + ObjectId)) def test_serialize_inside_nested_x_of_rules(self): schema = { diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 78417d9a9..e14959410 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -26,7 +26,8 @@ def test_bulk_delete_id_field(self): self.app.config["IF_MATCH"] = False products, _ = self.get(self.products) list_products_skus = [product["parent_product"] for product in - products["_items"] if "parent_product" in product] + products["_items"] if "parent_product" in + product] # Deletion of all the product in the first cart url = self.child_products_url.replace( '', list_products_skus[0]) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index dd937c38d..686b7d787 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1,5 +1,5 @@ from base64 import b64decode -from bson import ObjectId, decimal128 +from bson import ObjectId import simplejson as json diff --git a/tox.ini b/tox.ini index ddb22ebae..53e9426d6 100644 --- a/tox.ini +++ b/tox.ini @@ -7,13 +7,14 @@ commands=python setup.py test {posargs} [testenv:flake8] deps=flake8 basepython=python2 -commands=flake8 --ignore=E731 eve {posargs} +commands=flake8 --ignore=E731,E722 eve {posargs} [tox:travis] 2.6 = py26 2.7 = py27 3.3 = py33 3.4 = py34 -3.5 = py35,flake +3.5 = py35 3.6 = py36 pypy = pypy +flake = flake8 From 59cda9737962673d40e45f160a4b27d514c3e47e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 21 Nov 2017 11:46:47 +0100 Subject: [PATCH 227/821] Improve robustness on ObjectId validation --- eve/io/mongo/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 312295a66..7fc59c9bf 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -164,7 +164,7 @@ def _validate_data_relation(self, data_relation, field, value): data_resource, data_relation['field'])) def _validate_type_objectid(self, value): - if isinstance(value, ObjectId): + if ObjectId.is_valid(value): return True def _validate_type_decimal(self, value): From 403b85a264ca4159dbab12ce41bdc61b2eac2f69 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 21 Nov 2017 11:55:33 +0100 Subject: [PATCH 228/821] flake8 runs on CI with py35 --- tox.ini | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index ddbc519cc..8f980c00c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] -envlist=py26,py27,py33,py34,py35,py36,pypy,flake8 +envlist=py26,py27,py33,py34,py35,py36,pypy [testenv] commands=python setup.py test {posargs} [testenv:flake8] deps=flake8 -basepython=python2 -commands=flake8 --ignore=E731,E722 eve {posargs} +basepython=python3 +commands=flake8 --ignore=E731,E722,F821 eve {posargs} [tox:travis] 2.6 = py26 2.7 = py27 3.3 = py33 3.4 = py34 -3.5 = py35 +3.5 = py35, flake8 3.6 = py36 pypy = pypy -flake8 = flake8 From 614a64191985450ff114fc9c060e74564ae036ad Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 21 Nov 2017 15:40:34 +0100 Subject: [PATCH 229/821] Remove flake8 from travis matrix --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0202d3242..afb56f0b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ python: - 3.5 - 3.6 - pypy - - flake8 install: travis_retry pip install tox-travis services: #- mongodb From b608e1b4dd92717118bf8218f713573f394d6482 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 23 Nov 2017 10:18:00 +0100 Subject: [PATCH 230/821] Fix: serialization failure with schema registries. When Cerberus registry schemas are used, serialization might silently fail since the serialize() method does not expand registered schemas and rules. --- eve/methods/common.py | 50 +++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index fc7729ddc..2ffb666a9 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -10,32 +10,29 @@ :license: BSD, see LICENSE for more details. """ import base64 -try: - from collections import Counter -except: - from backport_collections import Counter -import simplejson as json import time - -from bson.dbref import DBRef -from bson.errors import InvalidId from copy import copy from datetime import datetime -from eve.utils import auto_fields -from eve.utils import config -from eve.utils import debug_error_message -from eve.utils import document_etag -from eve.utils import parse_request -from eve.versioning import get_data_version_relation_document -from eve.versioning import resolve_document_version -from flask import Response -from flask import abort -from flask import current_app as app -from flask import g -from flask import request from functools import wraps + +import simplejson as json +from bson.dbref import DBRef +from bson.errors import InvalidId +from cerberus import schema_registry, rules_set_registry +from flask import Response, abort, current_app as app, g, request from werkzeug.datastructures import MultiDict, CombinedMultiDict +from eve.utils import auto_fields, config, debug_error_message, \ + document_etag, parse_request +from eve.versioning import get_data_version_relation_document, \ + resolve_document_version + + +try: + from collections import Counter +except: + from backport_collections import Counter + def get_document(resource, concurrency_check, original=None, **lookup): """ Retrieves and return a single document. Since this function is used by @@ -374,11 +371,15 @@ def serialize(document, resource=None, schema=None, fields=None): .. versionadded:: 0.1.1 """ + def resolve_schema(schema): + return schema if isinstance(schema, dict) else \ + schema_registry.get(schema) + normalize_dotted_fields(document) if app.data.serializers: if resource: - schema = config.DOMAIN[resource]['schema'] + schema = resolve_schema(config.DOMAIN[resource]['schema']) if not fields: fields = document.keys() for field in fields: @@ -386,6 +387,8 @@ def serialize(document, resource=None, schema=None, fields=None): continue if field in schema: field_schema = schema[field] + if not isinstance(field_schema, dict): + field_schema = rules_set_registry.get(field_schema) field_type = field_schema.get('type') for x_of in ['allof', 'anyof', 'oneof', 'noneof']: for optschema in field_schema.get(x_of, []): @@ -402,7 +405,7 @@ def serialize(document, resource=None, schema=None, fields=None): if not isinstance(document[field], list): document[field] = [document[field]] if 'schema' in field_schema: - field_schema = field_schema['schema'] + field_schema = resolve_schema(field_schema['schema']) if 'dict' in (field_type, field_schema.get('type')): # either a dict or a list of dicts embedded = [document[field]] if field_type == 'dict' \ @@ -420,7 +423,8 @@ def serialize(document, resource=None, schema=None, fields=None): serialize(subdocument, schema=field_schema) elif field_schema.get('type') == 'list': # a list of lists - sublist_schema = field_schema.get('schema') + sublist_schema = resolve_schema( + field_schema.get('schema')) item_type = sublist_schema.get('type') for sublist in document[field]: for i, v in enumerate(sublist): From 29e436117ca52cc494465a833ae10d4587dd2755 Mon Sep 17 00:00:00 2001 From: Moritz Schneider Date: Sun, 3 Dec 2017 13:30:00 +0100 Subject: [PATCH 231/821] Fix sanitization of nested queries. A query was not fully traversed in the sanitization. Therefore the blacklist for mongo wueries could be bypassed, allowing for dangerous "$where" queries. --- AUTHORS | 1 + eve/io/mongo/mongo.py | 12 ++++++++---- eve/tests/methods/get.py | 9 +++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4e4b3b0b1..48aa79d84 100644 --- a/AUTHORS +++ b/AUTHORS @@ -103,6 +103,7 @@ Patches and Contributions - Mattias Lundberg - Mayur Dhamanwala - Mikael Berg +- Moritz Schneider - Mugur Rus - Nathan Reynolds - Niall Donegan diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 3ae38b88c..83b2ceba4 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -808,10 +808,14 @@ def sanitize_keys(spec): 'Query contains operators banned in MONGO_QUERY_BLACKLIST' )) - sanitize_keys(spec) - for value in spec.values(): - if isinstance(value, dict): - sanitize_keys(value) + if isinstance(spec, dict): + sanitize_keys(spec) + for value in spec.values(): + self._sanitize(value) + if isinstance(spec, list): + for value in spec: + self._sanitize(value) + return spec def _wc(self, resource): diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index bc88b82b5..ee39ce169 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -205,6 +205,15 @@ def test_get_mongo_query_blacklist(self): _, status = self.get(self.known_resource, '?where=%s' % where) self.assert400(status) + def test_get_mongo_query_blacklist_nested(self): + where = '{"$or": [{"$where": "this.ref == ''%s''"}]}' % self.item_name + _, status = self.get(self.known_resource, '?where=%s' % where) + self.assert400(status) + + where = '{"$or": [{"ref": {"$regex": "%s"}}]}' % self.item_name + _, status = self.get(self.known_resource, '?where=%s' % where) + self.assert400(status) + def test_get_where_mongo_objectid_as_string(self): where = '{"tid": "%s"}' % self.item_tid response, status = self.get(self.known_resource, '?where=%s' % where) From 532968da185812e593ccf50f49478042eda50654 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 09:33:00 +0100 Subject: [PATCH 232/821] Bump version to 0.7.5 --- CHANGES | 6 ++++++ eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 41a1c0fb9..8871c0712 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,12 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Version 0.7.5 +~~~~~~~~~~~~~ + +Not yet released. + + Stable ------ diff --git a/eve/__init__.py b/eve/__init__.py index dbe6722f8..a6c7e00a1 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.4' +__version__ = '0.7.5' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 017646734..7af936b91 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.4', + version='0.7.5', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From a5c96d794ddace7b5906cd76b11157a1c71905fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 09:34:20 +0100 Subject: [PATCH 233/821] Changelog for #1091 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 8871c0712..01060b2d5 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,9 @@ Version 0.7.5 Not yet released. +- Fix: A query was not fully traversed in the sanitization. Therefore the + blacklist for mongo wueries could be bypassed, allowing for dangerous + ``$where`` queries (Moritz Schneider). Stable ------ From 3c8264e6c9953b400aac1c74bc13f7a79e0a0130 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 09:35:21 +0100 Subject: [PATCH 234/821] Moritz Schneider --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 48aa79d84..0dc814d15 100644 --- a/AUTHORS +++ b/AUTHORS @@ -104,6 +104,7 @@ Patches and Contributions - Mayur Dhamanwala - Mikael Berg - Moritz Schneider +- Moritz Schneider - Mugur Rus - Nathan Reynolds - Niall Donegan From defdccd91c533eebd92504df1bc2619de7a8a755 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 09:41:41 +0100 Subject: [PATCH 235/821] v0.7.5 release date --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 01060b2d5..60ddf352e 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,7 @@ Development Version 0.7.5 ~~~~~~~~~~~~~ -Not yet released. +Released on 4 December, 2017 - Fix: A query was not fully traversed in the sanitization. Therefore the blacklist for mongo wueries could be bypassed, allowing for dangerous From ba7d9cf46a9850682ab1b1a551b3182437e048bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 10:29:26 +0100 Subject: [PATCH 236/821] Add sphinxcontrib-embedly to dev-requirements.txt --- CHANGES | 1 + dev-requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 15b65d82a..f4cf6712f 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: add sphinxcontrib-embedly to dev-requirements.txt. - New: when the media endpoint is enabled, the default authentication class will be used to secure it. Closes #1083. - New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, diff --git a/dev-requirements.txt b/dev-requirements.txt index 6ed6c6f89..75270836c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -13,4 +13,5 @@ Sphinx==1.2.3 tox==2.4.1 wheel==0.24.0 testfixtures==4.1.2 -alabaster==0.7.10 \ No newline at end of file +alabaster==0.7.10 +sphinxcontrib-embedly From 06cf1aff028bd03957e57268b900ef7054f50eea Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Dec 2017 10:49:23 +0100 Subject: [PATCH 237/821] Add Codemotion Rome 2017 to sessions list --- docs/rest_api_for_humans.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index 56c5e20be..6f4c65a5f 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -12,6 +12,7 @@ Conferences ------------ Eve REST API for Humans™ has been presented at the following events so far: +- Codemotion 2017, Rome - PiterPy 2016, St. Petersburg - Percona Live 2015, Amsterdam - EuroPython 2014, Berlin From fd9b05150befba269aaf5454c5658c7331eb1461 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 6 Dec 2017 18:01:21 +0100 Subject: [PATCH 238/821] Remove twitter and add StackOverflow from support options --- docs/support.rst | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/support.rst b/docs/support.rst index 836761b67..ddc430740 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -2,7 +2,14 @@ Support ======= -If you have any questions or issues about Eve, there are several options: +Please keep in mind that the issues on GitHub are reserved for bugs and +feature requests. If you have general or usage questions about Eve, there +are several options: + +Stack Overflow +-------------- +`Stack Overflow`_ has a eve tag. It is generally followed by Eve developers +and users. Mailing List ------------ @@ -10,20 +17,16 @@ The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting feedback. +IRC +--- +There is an official Freenode channel for Eve at `#python-eve +`_. + File an Issue ------------- If you notice some unexpected behavior in Eve, or want to see support for a new feature, `file an issue on GitHub `_. -Send a Tweet ------------- -If your question is less than 140 characters, feel free to send a tweet to -`@nicolaiarocci `_. - -IRC ---- -There is an official Freenode channel for Eve at `#python-eve -`_. - .. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve +.. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/eve From 710c34fba1f4a1e2ee7a45021f18988feed88728 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 7 Dec 2017 09:18:47 +0100 Subject: [PATCH 239/821] Move index creation logic in its own isolated and importable method --- CHANGES | 6 ++++++ eve/flaskapp.py | 14 +++----------- eve/io/mongo/__init__.py | 2 +- eve/io/mongo/mongo.py | 35 +++++++++++++++++++++++++++++++---- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index f4cf6712f..0795179fa 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,12 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: Refactor index creation. We now have a new + ``eve.io.mongo.ensure_mongo_indexes()`` function which ensures that eventual + ``mongo_indexes`` defined for a resource are created on the active + database. The function can be imported and invoked, for example in + multi-db workflows where a db is activated based on the + authenticated user performing the request (via custom auth classes). - Fix: add sphinxcontrib-embedly to dev-requirements.txt. - New: when the media endpoint is enabled, the default authentication class will be used to secure it. Closes #1083. diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 7cc70e667..ee592cbe8 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -24,7 +24,8 @@ error_endpoint, media_endpoint, schema_collection_endpoint, \ schema_item_endpoint from eve.exceptions import ConfigException, SchemaException -from eve.io.mongo import Mongo, Validator, GridFSMediaStorage, create_index +from eve.io.mongo import Mongo, Validator, GridFSMediaStorage, \ + ensure_mongo_indexes from eve.logging import RequestFilter from eve.utils import api_prefix, extract_key_values @@ -894,16 +895,7 @@ def register_resource(self, resource, settings): ) # create the mongo db indexes - mongo_indexes = self.config['DOMAIN'][resource]['mongo_indexes'] - if mongo_indexes: - for name, value in mongo_indexes.items(): - if isinstance(value, tuple): - list_of_keys, index_options = value - else: - list_of_keys = value - index_options = {} - - create_index(self, resource, name, list_of_keys, index_options) + ensure_mongo_indexes(self, resource) # flask-pymongo compatibility. if 'MONGO_OPTIONS' in self.config['DOMAIN']: diff --git a/eve/io/mongo/__init__.py b/eve/io/mongo/__init__.py index e5d2c6d0e..c96be063f 100644 --- a/eve/io/mongo/__init__.py +++ b/eve/io/mongo/__init__.py @@ -11,6 +11,6 @@ """ # flake8: noqa -from eve.io.mongo.mongo import Mongo, MongoJSONEncoder, create_index +from eve.io.mongo.mongo import Mongo, MongoJSONEncoder, ensure_mongo_indexes from eve.io.mongo.validation import Validator from eve.io.mongo.media import GridFSMediaStorage diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 8b0ec5395..ce8e79c4d 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -943,7 +943,27 @@ def db(self): return self.mongo.pymongo().db -def create_index(app, resource, name, list_of_keys, index_options): +def ensure_mongo_indexes(app, resource): + """ Make sure 'mongo_indexes' is respected and mongo indexes are created on + the current database. + + .. versionaddded:: 0.8 + """ + mongo_indexes = app.config['DOMAIN'][resource]['mongo_indexes'] + if not mongo_indexes: + return + + for name, value in mongo_indexes.items(): + if isinstance(value, tuple): + list_of_keys, index_options = value + else: + list_of_keys = value + index_options = {} + + _create_index(app, resource, name, list_of_keys, index_options) + + +def _create_index(app, resource, name, list_of_keys, index_options): """ Create a specific index composed of the `list_of_keys` for the mongo collection behind the `resource` using the `app.config` to retrieve all data needed to find out the mongodb configuration. @@ -965,6 +985,7 @@ def create_index(app, resource, name, list_of_keys, index_options): {"sparse": True} .. versionadded:: 0.6 + """ # it doesn't work as a typical mongodb method run in the request # life cycle, it is just called when the app start and it uses @@ -972,7 +993,12 @@ def create_index(app, resource, name, list_of_keys, index_options): collection = app.config['SOURCES'][resource]['source'] # get db for given prefix - px = app.config['DOMAIN'][resource].get('mongo_prefix', 'MONGO') + try: + # mongo_prefix might have been set by Auth class instance + px = g.get('mongo_prefix') + except: + px = app.config['DOMAIN'][resource].get('mongo_prefix', 'MONGO') + with app.app_context(): db = app.data.pymongo(resource, px).db @@ -989,8 +1015,9 @@ def create_index(app, resource, name, list_of_keys, index_options): except pymongo.errors.OperationFailure as e: if e.code == 85: # This error is raised when the definition of the index has - # been changed, we didn't found any spec out there but we think - # that this error is not going to change and we can trust. + # been changed, we didn't find any spec out there but we + # think that this error is not going to change and we can + # trust. # by default, drop the old index with old configuration and # create the index again with the new configuration. From f8f7019ffdf9b4e05faf95e1f04e204aa4c91f98 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Jan 2018 17:51:26 +0100 Subject: [PATCH 240/821] fix mongo visitor parser --- eve/io/mongo/parser.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index 6b751203b..ac633c098 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -122,16 +122,19 @@ def visit_Call(self, node): datetime(). """ if isinstance(node.func, ast.Name): - expr = None if node.func.id == 'ObjectId': - expr = "('" + node.args[0].s + "')" + try: + self.current_value = ObjectId(node.args[0].s) + except: + pass elif node.func.id == 'datetime': values = [] for arg in node.args: - values.append(str(arg.n)) - expr = "(" + ", ".join(values) + ")" - if expr: - self.current_value = eval(node.func.id + expr) + values.append(arg.n) + try: + self.current_value = datetime(*values) + except: + pass def visit_Attribute(self, node): """ Attribute handler ('Contact.Id'). From 8a13312057a2683492e61d4c4a4b661963de39e0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Jan 2018 18:09:08 +0100 Subject: [PATCH 241/821] Orange Tsai --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 5f6bef7cc..525240b20 100644 --- a/AUTHORS +++ b/AUTHORS @@ -120,6 +120,7 @@ Patches and Contributions - Olivier Poitrey - Ondrej Slinták - Or Neeman +- Orange Tsai - Pahaz Blinov - Patrick Decat - Pau Freixes From 57c320b925164dbc579355b0a06affa903f2ca1f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Jan 2018 17:51:26 +0100 Subject: [PATCH 242/821] fix mongo visitor parser --- eve/io/mongo/parser.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index 6b751203b..ac633c098 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -122,16 +122,19 @@ def visit_Call(self, node): datetime(). """ if isinstance(node.func, ast.Name): - expr = None if node.func.id == 'ObjectId': - expr = "('" + node.args[0].s + "')" + try: + self.current_value = ObjectId(node.args[0].s) + except: + pass elif node.func.id == 'datetime': values = [] for arg in node.args: - values.append(str(arg.n)) - expr = "(" + ", ".join(values) + ")" - if expr: - self.current_value = eval(node.func.id + expr) + values.append(arg.n) + try: + self.current_value = datetime(*values) + except: + pass def visit_Attribute(self, node): """ Attribute handler ('Contact.Id'). From ee4c5580a6e061688b13d5a4e7f95af2b3b1c8ee Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Jan 2018 18:09:08 +0100 Subject: [PATCH 243/821] Orange Tsai --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 0dc814d15..f823237fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -116,6 +116,8 @@ Patches and Contributions - Olivier Poitrey - Ondrej Slinták - Or Neeman +- Orange Tsai +- Pahaz Blinov - Patrick Decat - Pau Freixes - Paul Doucet From 315df991a1abdf8aa68eedabec72c222e6fac56d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Jan 2018 18:20:26 +0100 Subject: [PATCH 244/821] Bump version to 0.7.6 --- CHANGES | 13 +++++++++++-- eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 60ddf352e..588299da3 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,17 @@ Here you can see the full list of changes between each Eve release. Development ----------- + +Stable +------ + +Version 0.7.6 +~~~~~~~~~~~~~ + +Released on 14 January, 2018 + +- Improve query parsing robustness. + Version 0.7.5 ~~~~~~~~~~~~~ @@ -15,8 +26,6 @@ Released on 4 December, 2017 blacklist for mongo wueries could be bypassed, allowing for dangerous ``$where`` queries (Moritz Schneider). -Stable ------- Version 0.7.4 ~~~~~~~~~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index a6c7e00a1..5a43debf5 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.5' +__version__ = '0.7.6' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 7af936b91..4240d1f53 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.5', + version='0.7.6', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From ccfc2d722faaed480107a64f82a7021fc1dce3be Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Fri, 15 Dec 2017 16:20:11 +0000 Subject: [PATCH 245/821] Extendable rendering classes --- eve/default_settings.py | 4 + eve/render.py | 407 +++++++++++++++++++++------------------- eve/tests/renders.py | 7 +- eve/tests/utils.py | 6 +- eve/utils.py | 14 ++ tox.ini | 2 +- 6 files changed, 242 insertions(+), 198 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index 4e4cb68d7..68285ed8c 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -152,6 +152,10 @@ VALIDATE_FILTERS = False SORTING = True # sorting enabled by default. JSON_SORT_KEYS = False # json key sorting +RENDERERS = ( + 'eve.render.JSONRenderer', + 'eve.render.XMLRenderer' +) EMBEDDING = True # embedding enabled by default PROJECTION = True # projection enabled by default PAGINATION = True # pagination enabled by default. diff --git a/eve/render.py b/eve/render.py index 0ddcdde79..ec3339eda 100644 --- a/eve/render.py +++ b/eve/render.py @@ -18,7 +18,7 @@ from functools import wraps from eve.methods.common import get_rate_limit from eve.utils import date_to_str, date_to_rfc1123, config, \ - debug_error_message + debug_error_message, import_from_string from flask import make_response, request, Response, current_app as app, abort try: @@ -27,12 +27,6 @@ # Python 2.6 needs this back-port from backport_collections import OrderedDict -# mapping between supported mime types and render functions. -_MIME_TYPES = [ - {'mime': ('application/json',), 'renderer': 'render_json', 'tag': 'JSON'}, - {'mime': ('application/xml', 'text/xml', 'application/x-xml',), - 'renderer': 'render_xml', 'tag': 'XML'}] - def raise_event(f): """ Raises both general and resource-level events after the decorated @@ -144,10 +138,10 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, else: # obtain the best match between client's request and available mime # types, along with the corresponding render function. - mime, renderer = _best_mime() + mime, renderer_cls = _best_mime() # invoke the render function and obtain the corresponding rendered item - rendered = globals()[renderer](dct) + rendered = renderer_cls().render(dct) # JSONP if config.JSONP_ARGUMENT: @@ -267,12 +261,11 @@ def _best_mime(): """ supported = [] renders = {} - for mime in _MIME_TYPES: - # only mime types that have not been disabled via configuration - if app.config.get(mime['tag'], True): - for mime_type in mime['mime']: - supported.append(mime_type) - renders[mime_type] = mime['renderer'] + for renderer_cls in app.config.get('RENDERERS'): + renderer = import_from_string(renderer_cls) + for mime_type in renderer.mime: + supported.append(mime_type) + renders[mime_type] = renderer if len(supported) == 0: abort(500, description=debug_error_message( @@ -284,197 +277,227 @@ def _best_mime(): return best_match, renders[best_match] -def render_json(data): - """ JSON render function - - .. versionchanged:: 0.2 - Json encoder class is now inferred by the active data layer, allowing - for customized, data-aware JSON encoding. - - .. versionchanged:: 0.1.0 - Support for optional HATEOAS. - """ - set_indent = None - - # make pretty prints available - if 'GET' in request.method and 'pretty' in request.args: - set_indent = 4 - return json.dumps(data, indent=set_indent, cls=app.data.json_encoder_class, - sort_keys=config.JSON_SORT_KEYS) - - -def render_xml(data): - """ XML render function. - - :param data: the data stream to be rendered as xml. - - .. versionchanged:: 0.4 - Support for pagination info (_meta). - - .. versionchanged:: 0.2 - Use the new ITEMS configuration setting. - - .. versionchanged:: 0.1.0 - Support for optional HATEOAS. - - .. versionchanged:: 0.0.3 - Support for HAL-like hyperlinks and resource descriptors. - """ - if isinstance(data, list): - data = {config.ITEMS: data} - - xml = '' - if data: - xml += xml_root_open(data) - xml += xml_add_links(data) - xml += xml_add_meta(data) - xml += xml_add_items(data) - xml += xml_root_close() - return xml - - -def xml_root_open(data): - """ Returns the opening tag for the XML root node. If the datastream - includes informations about resource endpoints (href, title), they will - be added as node attributes. The resource endpoint is then removed to allow - for further processing of the datastream. - - :param data: the data stream to be rendered as xml. - - .. versionchanged:: 0.1.0 - Support for optional HATEOAS. - - .. versionchanged:: 0.0.6 - Links are now properly escaped. - - .. versionadded:: 0.0.3 - """ - links = data.get(config.LINKS) - href = title = '' - if links and 'self' in links: - self_ = links.pop('self') - href = ' href="%s" ' % utils.escape(self_['href']) - if 'title' in self_: - title = ' title="%s" ' % self_['title'] - return '' % (href, title) - - -def xml_add_meta(data): - """ Returns a meta node with page, total, max_results fields. - - :param data: the data stream to be rendered as xml. - - .. versionchanged:: 0.5 - Always return ordered items (#441). - - .. versionadded:: 0.4 - """ - xml = '' - meta = [] - if data.get(config.META): - ordered_meta = OrderedDict(sorted(data[config.META].items())) - for name, value in ordered_meta.items(): - meta.append('<%s>%d' % (name, value, name)) - if meta: - xml = '<%s>%s' % (config.META, ''.join(meta), config.META) - return xml - - -def xml_add_links(data): - """ Returns as many nodes as there are in the datastream. The links - are then removed from the datastream to allow for further processing. +class Renderer(object): + """ Base class for all the renderers. Renderer should set valid `mime` attr + and have `.render()` method implemented. - :param data: the data stream to be rendered as xml. - - .. versionchanged:: 0.5 - Always return ordered items (#441). - - .. versionchanged:: 0.0.6 - Links are now properly escaped. - - .. versionadded:: 0.0.3 """ - xml = '' - chunk = '' - links = data.pop(config.LINKS, {}) - ordered_links = OrderedDict(sorted(links.items())) - for rel, link in ordered_links.items(): - if isinstance(link, list): - xml += ''.join([chunk % (rel, utils.escape(d['href']), - utils.escape(d['title'])) for d in link]) - else: - xml += ''.join(chunk % (rel, utils.escape(link['href']), - link['title'])) - return xml + mime = tuple() + def render(self, data): + raise NotImplementedError('Renderer .render() method is not ' + 'implemented') -def xml_add_items(data): - """ When this function is called the datastream can only contain a `_items` - list, or a dictionary. If a list, each item is a resource which rendered as - XML. If a dictionary, it will be rendered as XML. - :param data: the data stream to be rendered as xml. +class JSONRenderer(Renderer): + """ JSON renderer class - .. versionadded:: 0.0.3 """ - try: - xml = ''.join([xml_item(item) for item in data[config.ITEMS]]) - except: - xml = xml_dict(data) - return xml + mime = ('application/json',) + def render(self, data): + """ JSON render function -def xml_item(item): - """ Represents a single resource (member of a collection) as XML. + :param data: the data stream to be rendered as json. - :param data: the data stream to be rendered as xml. + .. versionchanged:: 0.2 + Json encoder class is now inferred by the active data layer, allowing + for customized, data-aware JSON encoding. - .. versionadded:: 0.0.3 - """ - xml = xml_root_open(item) - xml += xml_add_links(item) - xml += xml_dict(item) - xml += xml_root_close() - return xml + .. versionchanged:: 0.1.0 + Support for optional HATEOAS. + """ + set_indent = None + # make pretty prints available + if 'GET' in request.method and 'pretty' in request.args: + set_indent = 4 + return json.dumps(data, indent=set_indent, + cls=app.data.json_encoder_class, + sort_keys=config.JSON_SORT_KEYS) -def xml_root_close(): - """ Returns the closing tag of the XML root node. - .. versionadded:: 0.0.3 - """ - return '
    ' - - -def xml_dict(data): - """ Renders a dict as XML. - - :param data: the data stream to be rendered as xml. - - .. versionchanged:: 0.5 - Always return ordered items (#441). - - .. versionchanged:: 0.2 - Leaf values are now properly escaped. +class XMLRenderer(Renderer): + """ XML renderer class - .. versionadded:: 0.0.3 """ - xml = '' - ordered_items = OrderedDict(sorted(data.items())) - for k, v in ordered_items.items(): - if isinstance(v, datetime.datetime): - v = date_to_str(v) - elif isinstance(v, (datetime.time, datetime.date)): - v = v.isoformat() - if not isinstance(v, list): - v = [v] - for value in v: - if isinstance(value, dict): - links = xml_add_links(value) - xml += "<%s>" % k - xml += xml_dict(value) - xml += links - xml += "" % k + mime = ('application/xml', 'text/xml', 'application/x-xml',) + tag = 'XML' + + def render(self, data): + """ XML render function. + + :param data: the data stream to be rendered as xml. + + .. versionchanged:: 0.4 + Support for pagination info (_meta). + + .. versionchanged:: 0.2 + Use the new ITEMS configuration setting. + + .. versionchanged:: 0.1.0 + Support for optional HATEOAS. + + .. versionchanged:: 0.0.3 + Support for HAL-like hyperlinks and resource descriptors. + """ + if isinstance(data, list): + data = {config.ITEMS: data} + + xml = '' + if data: + xml += self.xml_root_open(data) + xml += self.xml_add_links(data) + xml += self.xml_add_meta(data) + xml += self.xml_add_items(data) + xml += self.xml_root_close() + return xml + + @classmethod + def xml_root_open(cls, data): + """ Returns the opening tag for the XML root node. If the datastream + includes informations about resource endpoints (href, title), they will + be added as node attributes. The resource endpoint is then removed to + allow for further processing of the datastream. + + :param data: the data stream to be rendered as xml. + + .. versionchanged:: 0.1.0 + Support for optional HATEOAS. + + .. versionchanged:: 0.0.6 + Links are now properly escaped. + + .. versionadded:: 0.0.3 + """ + links = data.get(config.LINKS) + href = title = '' + if links and 'self' in links: + self_ = links.pop('self') + href = ' href="%s" ' % utils.escape(self_['href']) + if 'title' in self_: + title = ' title="%s" ' % self_['title'] + return '' % (href, title) + + @classmethod + def xml_add_meta(cls, data): + """ Returns a meta node with page, total, max_results fields. + + :param data: the data stream to be rendered as xml. + + .. versionchanged:: 0.5 + Always return ordered items (#441). + + .. versionadded:: 0.4 + """ + xml = '' + meta = [] + if data.get(config.META): + ordered_meta = OrderedDict(sorted(data[config.META].items())) + for name, value in ordered_meta.items(): + meta.append('<%s>%d' % (name, value, name)) + if meta: + xml = '<%s>%s' % (config.META, ''.join(meta), config.META) + return xml + + @classmethod + def xml_add_links(cls, data): + """ Returns as many nodes as there are in the datastream. The + links are then removed from the datastream to allow for further + processing. + + :param data: the data stream to be rendered as xml. + + .. versionchanged:: 0.5 + Always return ordered items (#441). + + .. versionchanged:: 0.0.6 + Links are now properly escaped. + + .. versionadded:: 0.0.3 + """ + xml = '' + chunk = '' + links = data.pop(config.LINKS, {}) + ordered_links = OrderedDict(sorted(links.items())) + for rel, link in ordered_links.items(): + if isinstance(link, list): + xml += ''.join([chunk % (rel, utils.escape(d['href']), + utils.escape(d['title'])) + for d in link]) else: - xml += "<%s>%s" % (k, utils.escape(value), k) - return xml + xml += ''.join(chunk % (rel, utils.escape(link['href']), + link['title'])) + return xml + + @classmethod + def xml_add_items(cls, data): + """ When this function is called the datastream can only contain + a `_items` list, or a dictionary. If a list, each item is a resource + which rendered as XML. If a dictionary, it will be rendered as XML. + + :param data: the data stream to be rendered as xml. + + .. versionadded:: 0.0.3 + """ + try: + xml = ''.join([cls.xml_item(item) for item in data[config.ITEMS]]) + except: + xml = cls.xml_dict(data) + return xml + + @classmethod + def xml_item(cls, item): + """ Represents a single resource (member of a collection) as XML. + + :param data: the data stream to be rendered as xml. + + .. versionadded:: 0.0.3 + """ + xml = cls.xml_root_open(item) + xml += cls.xml_add_links(item) + xml += cls.xml_dict(item) + xml += cls.xml_root_close() + return xml + + @classmethod + def xml_root_close(cls): + """ Returns the closing tag of the XML root node. + + .. versionadded:: 0.0.3 + """ + return '' + + @classmethod + def xml_dict(cls, data): + """ Renders a dict as XML. + + :param data: the data stream to be rendered as xml. + + .. versionchanged:: 0.5 + Always return ordered items (#441). + + .. versionchanged:: 0.2 + Leaf values are now properly escaped. + + .. versionadded:: 0.0.3 + """ + xml = '' + ordered_items = OrderedDict(sorted(data.items())) + for k, v in ordered_items.items(): + if isinstance(v, datetime.datetime): + v = date_to_str(v) + elif isinstance(v, (datetime.time, datetime.date)): + v = v.isoformat() + if not isinstance(v, list): + v = [v] + for value in v: + if isinstance(value, dict): + links = cls.xml_add_links(value) + xml += "<%s>" % k + xml += cls.xml_dict(value) + xml += links + xml += "" % k + else: + xml += "<%s>%s" % (k, utils.escape(value), k) + return xml diff --git a/eve/tests/renders.py b/eve/tests/renders.py index fbdd6e776..8bbadf421 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -64,8 +64,7 @@ def test_unknown_render(self): self.assertEqual(r.content_type, 'application/json') def test_json_xml_disabled(self): - self.app.config['JSON'] = False - self.app.config['XML'] = False + self.app.config['RENDERERS'] = tuple() r = self.test_client.get(self.known_resource_url, headers=[('Accept', 'application/json')]) self.assert500(r.status_code) @@ -76,7 +75,7 @@ def test_json_xml_disabled(self): self.assert500(r.status_code) def test_json_disabled(self): - self.app.config['JSON'] = False + self.app.config['RENDERERS'] = ('eve.render.XMLRenderer',) r = self.test_client.get(self.known_resource_url, headers=[('Accept', 'application/json')]) self.assertTrue('application/xml' in r.content_type) @@ -87,7 +86,7 @@ def test_json_disabled(self): self.assertTrue('application/xml' in r.content_type) def test_xml_disabled(self): - self.app.config['XML'] = False + self.app.config['RENDERERS'] = ('eve.render.JSONRenderer',) r = self.test_client.get(self.known_resource_url, headers=[('Accept', 'application/xml')]) self.assertEqual(r.content_type, 'application/json') diff --git a/eve/tests/utils.py b/eve/tests/utils.py index fc2a66a50..4025bf05d 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -7,7 +7,7 @@ from eve.tests import TestBase from eve.utils import parse_request, str_to_date, config, weak_date, \ date_to_str, querydef, document_etag, extract_key_values, \ - debug_error_message, validate_filters + debug_error_message, validate_filters, import_from_string class TestUtils(TestBase): @@ -261,6 +261,10 @@ def test_validate_filters(self): {'$or': [{'key': 'val1'}, {'key': 'val2'}]}, self.known_resource) is None) + def test_import_from_string(self): + dt = import_from_string('datetime.datetime') + self.assertEqual(dt, datetime) + class DummyEvent(object): """ diff --git a/eve/utils.py b/eve/utils.py index 12a337554..ea9d185d6 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -11,6 +11,8 @@ """ import sys +from importlib import import_module + import eve import hashlib import werkzeug.exceptions @@ -448,3 +450,15 @@ def auto_fields(resource): # Base string type that is compatible with both Python 2.x and 3.x. str_type = str if sys.version_info[0] == 3 else basestring + + +def import_from_string(module_name): + """ Imports module using string + + """ + try: + modules = module_name.split('.') + module_path, attr = '.'.join(modules[:-1]), modules[-1] + return getattr(import_module(module_path), attr) + except (ImportError, AttributeError): + raise ImportError('Cannot import {}'.format(module_name)) diff --git a/tox.ini b/tox.ini index 8f980c00c..42d3296f1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py26,py27,py33,py34,py35,py36,pypy +envlist=py36 [testenv] commands=python setup.py test {posargs} From bcfbfde783080d5e3052ce79bc48c60fbef72e51 Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Fri, 15 Dec 2017 16:24:45 +0000 Subject: [PATCH 246/821] Mistakenly added tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42d3296f1..8f980c00c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py36 +envlist=py26,py27,py33,py34,py35,py36,pypy [testenv] commands=python setup.py test {posargs} From 2d883bb23f19158c3bcdc2490b08f5906be8648a Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Mon, 18 Dec 2017 11:12:26 +0000 Subject: [PATCH 247/821] Updated py2.6 dependencies --- py26-requirements.txt | 1 + setup.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/py26-requirements.txt b/py26-requirements.txt index 8851829aa..c0fe0c827 100644 --- a/py26-requirements.txt +++ b/py26-requirements.txt @@ -1,2 +1,3 @@ -r requirements.txt backport_collections==0.1 +importlib==1.0.4 \ No newline at end of file diff --git a/setup.py b/setup.py index 1f8c54395..4cc1b8036 100755 --- a/setup.py +++ b/setup.py @@ -19,9 +19,11 @@ try: from collections import Counter, OrderedDict # noqa + import importlib except ImportError: # Python 2.6 install_requires.append('backport_collections') + install_requires.append('importlib==1.0.4') setup( From 5b247cfef6f6c744dbee3cf1ccf19d7106f6e208 Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Mon, 18 Dec 2017 11:21:10 +0000 Subject: [PATCH 248/821] PEP8 compliance --- eve/render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/render.py b/eve/render.py index ec3339eda..9a095bbe0 100644 --- a/eve/render.py +++ b/eve/render.py @@ -301,8 +301,8 @@ def render(self, data): :param data: the data stream to be rendered as json. .. versionchanged:: 0.2 - Json encoder class is now inferred by the active data layer, allowing - for customized, data-aware JSON encoding. + Json encoder class is now inferred by the active data layer, + allowing for customized, data-aware JSON encoding. .. versionchanged:: 0.1.0 Support for optional HATEOAS. From 230e9c9d27db87bb600e6275c52fb2aa3c9c0367 Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Thu, 4 Jan 2018 14:50:10 +0100 Subject: [PATCH 249/821] Checking for deprecated features proposal --- eve/default_settings.py | 4 ++-- eve/flaskapp.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index 68285ed8c..ec6b693c2 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -152,10 +152,10 @@ VALIDATE_FILTERS = False SORTING = True # sorting enabled by default. JSON_SORT_KEYS = False # json key sorting -RENDERERS = ( +RENDERERS = [ 'eve.render.JSONRenderer', 'eve.render.XMLRenderer' -) +] EMBEDDING = True # embedding enabled by default PROJECTION = True # projection enabled by default PAGINATION = True # pagination enabled by default. diff --git a/eve/flaskapp.py b/eve/flaskapp.py index ee592cbe8..7a5bc1b2a 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -12,6 +12,7 @@ import fnmatch import os import sys +import warnings import copy from events import Events @@ -20,6 +21,7 @@ from werkzeug.serving import WSGIRequestHandler import eve +from eve import default_settings from eve.endpoints import collections_endpoint, item_endpoint, home_endpoint, \ error_endpoint, media_endpoint, schema_collection_endpoint, \ schema_item_endpoint @@ -261,6 +263,34 @@ def find_settings_file(file_name): 'connect', True ) + self.check_deprecated_features() + + def check_deprecated_features(self): + """ Method check for usage of deprecated features. + """ + + def deprecated_renderers_settings(): + """ Checks if JSON or XML setting is still being used instead of + RENDERERS and if so, compose new settings. + """ + msg = '{} setting is deprecated and will be removed' \ + ' in future release. Please use RENDERERS instead.' + + if 'JSON' in self.config or 'XML' in self.config: + self.config['RENDERERS'] = default_settings.RENDERERS.copy() + + if 'JSON' in self.config: + warnings.warn(msg.format('JSON')) + if not self.config['JSON']: + self.config['RENDERERS'].remove('eve.render.JSONRenderer') + + if 'XML' in self.config: + warnings.warn(msg.format('XML')) + if not self.config['XML']: + self.config['RENDERERS'].remove('eve.render.XMLRenderer') + + deprecated_renderers_settings() + def validate_domain_struct(self): """ Validates that Eve configuration settings conform to the requirements. From a519ba36d3fde20488e88931832397707bc67f9d Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Thu, 4 Jan 2018 14:50:45 +0100 Subject: [PATCH 250/821] Checking for deprecated features proposal - typo fix --- eve/flaskapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 7a5bc1b2a..cead3f9d2 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -266,12 +266,12 @@ def find_settings_file(file_name): self.check_deprecated_features() def check_deprecated_features(self): - """ Method check for usage of deprecated features. + """ Method checks for usage of deprecated features. """ def deprecated_renderers_settings(): """ Checks if JSON or XML setting is still being used instead of - RENDERERS and if so, compose new settings. + RENDERERS and if so, composes new settings. """ msg = '{} setting is deprecated and will be removed' \ ' in future release. Please use RENDERERS instead.' From 5406d5dd85b34aa49e0a8ac33b1f3203a4d5aabb Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Mon, 15 Jan 2018 17:08:30 +0100 Subject: [PATCH 251/821] Updated docstrings --- eve/default_settings.py | 5 +++++ eve/render.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index ec6b693c2..aba35533f 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -11,6 +11,11 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. + .. versionchanged:: 0.8 + 'RENDERERS' added with XML and JSON renderers. + 'JSON' removed. + 'XML' removed. + .. versionchanged:: 0.7 'OPTIMIZE_PAGINATION_FOR_SPEED' added and set to False. 'OPLOG_RETURN_EXTRA_FIELD' added and set to False. diff --git a/eve/render.py b/eve/render.py index 9a095bbe0..436ad06d9 100644 --- a/eve/render.py +++ b/eve/render.py @@ -256,6 +256,10 @@ def _best_mime(): ones supported by Eve. Along with the mime, also the corresponding render function is returns. + .. versionchanged:: 0.8 + Support for optional renderers via RENDERERS. XML and JSON + configuration keywords removed. + .. versionchanged:: 0.3 Support for optional renderers via XML and JSON configuration keywords. """ @@ -278,8 +282,8 @@ def _best_mime(): class Renderer(object): - """ Base class for all the renderers. Renderer should set valid `mime` attr - and have `.render()` method implemented. + """ Base class for all the renderers. Renderer should set valid `mime` + attr and have `.render()` method implemented. """ mime = tuple() @@ -290,7 +294,7 @@ def render(self, data): class JSONRenderer(Renderer): - """ JSON renderer class + """ JSON renderer class based on `simplejson` package. """ mime = ('application/json',) @@ -318,7 +322,7 @@ def render(self, data): class XMLRenderer(Renderer): - """ XML renderer class + """ XML renderer class. """ mime = ('application/xml', 'text/xml', 'application/x-xml',) From e30c956c8fab3959023833c5a2e5255fdff125a2 Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Tue, 16 Jan 2018 17:56:39 +0100 Subject: [PATCH 252/821] Updated docs --- docs/config.rst | 11 ++--------- docs/features.rst | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index d588b4470..14f291cab 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -453,7 +453,6 @@ uppercase. ``META`` Allows to customize the meta field. Defaults to ``_meta`` - to ``_meta``. ``INFO`` String value to include an info section, with the given INFO name, at the Eve homepage (suggested @@ -474,15 +473,9 @@ uppercase. ``ENFORCE_IF_MATCH`` ``True`` to always enforce concurrency control when it is enabled, ``False`` otherwise. Defaults to - ``True``. See :ref:`concurrency`. -``XML`` ``True`` to enable XML support, ``False`` - otherwise. See :ref:`jsonxml`. Defaults to - ``True``. - -``JSON`` ``True`` to enable JSON support, ``False`` - otherwise. See :ref:`jsonxml`. Defaults to - ``True``. +``RENDERERS`` Allows to change enabled renderers. Defaults to + ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. ``JSON_SORT_KEYS`` ``True`` to enable JSON key sorting, ``False`` otherwise. Defaults to ``False``. diff --git a/docs/features.rst b/docs/features.rst index 34bc5bf26..b268728f2 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -488,9 +488,9 @@ want to turn HATEOAS off? Well, if you know that your client application is not going to use the feature, then you might want to save on both bandwidth and performance. -.. _jsonxml: +.. _rendering: -JSON and XML Rendering +Rendering ---------------------- Eve responses are automatically rendered as JSON (the default) or XML, depending on the request ``Accept`` header. Inbound documents (for inserts and @@ -510,10 +510,18 @@ edits) are in JSON format. -XML support can be disabled by setting ``XML`` to ``False`` in the settings -file. JSON support can be disabled by setting ``JSON`` to ``False``. Please -note that at least one mime type must always be enabled, either implicitly or -explicitly. By default, both are supported. +Default renderers might be changed by editing ``RENDERERS`` value in the settings file. + +.. code-block:: python + + RENDERERS = [ + 'eve.render.JSONRenderer', + 'eve.render.XMLRenderer' + ] + +You can create your own renderer by subclassing ``eve.render.Renderer``. Each +renderer should set valid ``mime`` attr and have ``.render()`` method implemented. +Please note that at least one renderer must always be enabled. .. _conditional_requests: From 0087bef0f8459006d2cc1094dfec0ad98f24539b Mon Sep 17 00:00:00 2001 From: Marcin Puhacz Date: Tue, 16 Jan 2018 17:12:19 +0000 Subject: [PATCH 253/821] Updated authors --- AUTHORS | 1 + docs/config.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 525240b20..15b8398a4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -94,6 +94,7 @@ Patches and Contributions - Mandar Vaze - Manquer - Marc Abramowitz +- Marcin Puhacz - Marcus Cobden - Marica Odagaki - Mario Kralj diff --git a/docs/config.rst b/docs/config.rst index 14f291cab..8bceb58e1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -473,6 +473,7 @@ uppercase. ``ENFORCE_IF_MATCH`` ``True`` to always enforce concurrency control when it is enabled, ``False`` otherwise. Defaults to + ``True``. See :ref:`concurrency`. ``RENDERERS`` Allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. From 31deee74fa5c16e6c23faf35f815b318d8ecf29b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 18 Jan 2018 09:28:16 +0100 Subject: [PATCH 254/821] Changelog for #1092 --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index 5ff8ea6c4..a07d1116c 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,14 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: Renderer classes. ``RENDERER`` allows to change enabled renderers. + Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You + can create your own renderer by subclassing ``eve.render.Renderer``. Each + renderer should set valid mime attr and have ``.render()`` method + implemented. Please note that at least one renderer must always be enabled + (Marcin Puhacz). +- Chane: ``JSON`` and ``XML`` settings are deprecated and will be removed in + a future update. Use ``RENDERERS`` instead (Marcin Puhacz). - New: Refactor index creation. We now have a new ``eve.io.mongo.ensure_mongo_indexes()`` function which ensures that eventual ``mongo_indexes`` defined for a resource are created on the active From 6a93aca5e2fb695a2ae909703bef5be90311abc1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Feb 2018 09:31:02 +0100 Subject: [PATCH 255/821] Add $geometry and $maxDistance to mongo operators Closes #1103. --- CHANGES | 8 ++++++++ eve/io/mongo/mongo.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 588299da3..be0857439 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,14 @@ Development Stable ------ +Version 0.7.7 +~~~~~~~~~~~~~ + +Released on 7 February, 2018 + +- Fix: geo queries now properly support ``$geometry`` and ``$maxDistance`` + operators. Closes #1103. + Version 0.7.6 ~~~~~~~~~~~~~ diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 83b2ceba4..8b8543831 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -98,7 +98,8 @@ class Mongo(DataLayer): ['$options', '$search', '$language'] + ['$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + - ['$all', '$elemMatch', '$size'] + ['$geometry', '$maxDistance'] + + ['$all', '$elemMatch', '$size'] + ) def init_app(self, app): From 5c3aa4bd949e90bc0156b0cb0e3ecb302ca40716 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Feb 2018 09:47:56 +0100 Subject: [PATCH 256/821] Bump version to 0.7.7 --- eve/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/__init__.py b/eve/__init__.py index 5a43debf5..bef049972 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.6' +__version__ = '0.7.7' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 4240d1f53..378158e9d 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.6', + version='0.7.7', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 73c5f1e70c34de501f19c427265b959782264678 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Feb 2018 14:44:06 +0100 Subject: [PATCH 257/821] Fix breaking syntax error in v0.7.7 --- eve/io/mongo/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 8b8543831..4c5eeed74 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -99,7 +99,7 @@ class Mongo(DataLayer): ['$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + ['$geometry', '$maxDistance'] + - ['$all', '$elemMatch', '$size'] + + ['$all', '$elemMatch', '$size'] ) def init_app(self, app): From afd573d9254a9a23393f35760e9c515300909ccd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Feb 2018 14:46:35 +0100 Subject: [PATCH 258/821] Bump version to 0.7.8 --- CHANGES | 7 +++++++ eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index be0857439..f249d1ddc 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,13 @@ Development Stable ------ +Version 0.7.8 +~~~~~~~~~~~~~ + +Released on 7 February, 2018 + +- Fix: breaking syntax error in v0.7.7 + Version 0.7.7 ~~~~~~~~~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index bef049972..7af370441 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.7.7' +__version__ = '0.7.8' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 378158e9d..b62f15aab 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='Eve', - version='0.7.7', + version='0.7.8', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 8df50c5dc791a82a1394a1fbb09a801fd5510bc8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 19 Feb 2018 11:37:03 +0100 Subject: [PATCH 259/821] Fix broken links in documentation/configuration page. --- CHANGES | 1 + docs/config.rst | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index fed1dd3ff..9b5d69170 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You can create your own renderer by subclassing ``eve.render.Renderer``. Each diff --git a/docs/config.rst b/docs/config.rst index 8bceb58e1..504090fb8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1298,43 +1298,43 @@ defining the field validation rules. Allowed validation rules are: ``valueschema`` Validation schema for all values of a ``dict``. The dict can have arbitrary keys, the values for all of which must validate with given - schema. See `valueschema example `_. + schema. See `valueschema `_ in Cerberus docs. ``keyschema`` This is the counterpart to ``valueschema`` that validates the keys of a dict. Validation schema for all values of a ``dict``. See - `keyschema example `_. + `keyschema `_ in Cerberus docs. ``regex`` Validation will fail if field value does not match the provided regex rule. Only applies to - string fields. See `email validation example `_ + string fields. See `regex `_ in Cerberus docs. ``dependencies`` This rule allows a list of fields that must be present in order for the target field to be - allowed. See `dependencies example `_ + allowed. See `dependencies `_ in Cerberus docs. ``anyof`` This rule allows you to list multiple sets of rules to validate against. The field will be considered valid if it validates against one - set in the list. See `anyof example `_ + set in the list. See `*of-rules `_ in Cerberus docs. ``allof`` Same as ``anyof``, except that all rule - collections in the list must validate. + collections in the list must validate. ``noneof`` Same as ``anyof``, except that it requires no rule collections in the list to validate. ``oneof`` Same as ``anyof``, except that only one rule - collections in the list can validate. + collections in the list can validate. ``coerce`` Type coercion allows you to apply a callable to a value before any other validators run. The return value of the callable replaces the new value in the document. This can be used to convert values or sanitize data before it is - validated. See `type coercion example `_ + validated. See `value coercion `_ in Cerberus docs. =============================== ============================================== From 9dd8db6ba2686498a17877d73ff495c4f1d1fc7d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Feb 2018 16:35:07 +0100 Subject: [PATCH 260/821] Bump Flask requirement to <=0.13 Closes #1111. --- CHANGES | 1 + requirements.txt | 6 +++--- setup.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 9b5d69170..9900499cb 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Update: bump Flask requirement to <=0.13. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You diff --git a/requirements.txt b/requirements.txt index bcad8e0d3..e536aae3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ Cerberus==1.1 Events==0.3 -Flask==0.12 +Flask==0.12.2 itsdangerous==0.24 -Jinja2==2.9.4 +Jinja2==2.10 MarkupSafe==0.23 pymongo==3.5.0 simplejson==3.8.2 -Werkzeug==0.11.15 +Werkzeug==0.14.1 diff --git a/setup.py b/setup.py index 4cc1b8036..a26d977c5 100755 --- a/setup.py +++ b/setup.py @@ -9,11 +9,11 @@ 'cerberus>=1.1', 'events>=0.3,<0.4', 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<=0.11.15', + 'werkzeug>=0.9.4,<=0.14', 'markupsafe>=0.23,<1.0', 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', - 'flask>=0.10.1,<=0.12', + 'flask>=0.10.1,<=0.13', 'pymongo>=3.5', ] From cbee8c4d6b0358d64de8b85135b9aed68fe2afbe Mon Sep 17 00:00:00 2001 From: kreynen Date: Thu, 18 Jan 2018 12:30:34 -0700 Subject: [PATCH 261/821] Fixed typo --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 9900499cb..94c5ee5b0 100644 --- a/CHANGES +++ b/CHANGES @@ -16,7 +16,7 @@ Version 0.8 renderer should set valid mime attr and have ``.render()`` method implemented. Please note that at least one renderer must always be enabled (Marcin Puhacz). -- Chane: ``JSON`` and ``XML`` settings are deprecated and will be removed in +- Change: ``JSON`` and ``XML`` settings are deprecated and will be removed in a future update. Use ``RENDERERS`` instead (Marcin Puhacz). - New: Refactor index creation. We now have a new ``eve.io.mongo.ensure_mongo_indexes()`` function which ensures that eventual From a7b4692a3a9ba3810f99c8085375596eb338aeef Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 24 Feb 2018 16:05:50 +0300 Subject: [PATCH 262/821] kreynen --- AUTHORS | 1 + CHANGES | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 15b8398a4..abee31a61 100644 --- a/AUTHORS +++ b/AUTHORS @@ -162,5 +162,6 @@ Patches and Contributions - Xavi Cubillas - boosh - dccrazyboy +- kreynen - mmizotin - xgdgsc diff --git a/CHANGES b/CHANGES index 94c5ee5b0..57b6e82e3 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix a changelog typo (kreynen). - Update: bump Flask requirement to <=0.13. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. From ce175e2840045c5889dedb4e0f608ce4adccde82 Mon Sep 17 00:00:00 2001 From: Olof Johansson Date: Thu, 22 Feb 2018 10:54:47 +0100 Subject: [PATCH 263/821] docs: Refer to example resources (contacts) consistently --- docs/features.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index b268728f2..9af6fb414 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1372,7 +1372,7 @@ the items as needed before they are returned to the client. >>> app.on_fetched_resource += before_returning_items >>> app.on_fetched_resource_contacts += before_returning_contacts >>> app.on_fetched_item += before_returning_item - >>> app.on_fetched_item_contact += before_returning_contact + >>> app.on_fetched_item_contacts += before_returning_contact It is important to note that fetch events will work with `Document Versioning`_ for specific document versions or accessing all document From 000f58fe473a5c54c500598417b6bded988405bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 25 Feb 2018 11:33:54 +0300 Subject: [PATCH 264/821] Olof Johansson --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index abee31a61..7ea253a4a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -119,6 +119,7 @@ Patches and Contributions - NotSpecial - Olivier Carrère - Olivier Poitrey +- Olof Johansson - Ondrej Slinták - Or Neeman - Orange Tsai From 4eb478b9044e4e3bcb43f135863755f8fcdf3791 Mon Sep 17 00:00:00 2001 From: Olof Johansson Date: Thu, 22 Feb 2018 11:40:49 +0100 Subject: [PATCH 265/821] docs: Use correct name for form-data mime type --- CHANGES | 2 +- docs/features.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 57b6e82e3..535e0eeed 100644 --- a/CHANGES +++ b/CHANGES @@ -1039,7 +1039,7 @@ Released on 14 February, 2014. - [new] media files (images, pdf, etc.) can be uploaded as ``media`` document fields. When a document is requested, eventual media files will be returned as Base64 strings. Upload is done via ``POST``, ``PUT`` and ``PATCH`` using - the ``multipart/data-form`` content-type. For optmized performance, by + the ``multipart/form-data`` content-type. For optmized performance, by default files are stored in GridFS, however custom ``MediaStorage`` classes can be provided to support alternative storage systems. Clients and API maintainers can exploit the projections feature to include/exclude media diff --git a/docs/features.rst b/docs/features.rst index 9af6fb414..8e8de04b4 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1578,7 +1578,7 @@ File Storage ------------ Media files (images, pdf, etc.) can be uploaded as ``media`` document fields. Upload is done via ``POST``, ``PUT`` and -``PATCH`` as usual, but using the ``multipart/data-form`` content-type. +``PATCH`` as usual, but using the ``multipart/form-data`` content-type. Let us assume that the ``accounts`` endpoint has a schema like this: @@ -1741,9 +1741,9 @@ response payloads by sending requests like this one: .. _multipart: -Note on media files as ``multipart/data-form`` +Note on media files as ``multipart/form-data`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you are uploading media files as ``multipart/data-form`` all the +If you are uploading media files as ``multipart/form-data`` all the additional fields except the file fields will be treated as ``strings`` for all field validation purposes. If you have already defined some of the resource fields to be of different type (boolean, number, list etc) From e819153a32ea6702ecdb1591bb6613bc4aa98214 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 25 Feb 2018 11:39:58 +0300 Subject: [PATCH 266/821] Changelog for #1114 and #1115. --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 535e0eeed..0dde81c1b 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix documentation typos (Olof Johansson) - Fix a changelog typo (kreynen). - Update: bump Flask requirement to <=0.13. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. From 6168ae9cb0c1ffcc9042669dd2f4e4aa9ec6c715 Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Fri, 23 Mar 2018 13:48:21 +0100 Subject: [PATCH 267/821] Fix VALIDATE_FILTERS behaviour for filters on sub-document fields (issue #1123) --- AUTHORS | 1 + eve/tests/methods/get.py | 30 ++++++++++++++++++++++ eve/tests/test_settings.py | 13 ++++++++++ eve/utils.py | 52 ++++++++++++++++++++++++++++++++------ 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7ea253a4a..3598fe274 100644 --- a/AUTHORS +++ b/AUTHORS @@ -89,6 +89,7 @@ Patches and Contributions - Kurt Bonne - Kurt Doherty - Luca Di Gaspero +- Luca Moretto - Luis Fernando Gomes - Magdas Adrian - Mandar Vaze diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 144ed6977..f21e171af 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1099,6 +1099,36 @@ def test_get_invalid_where_fields(self): response, status = self.get(self.known_resource, where) self.assert200(status) + # test for nested resource field validating correctly + # (location is dict) + where = '?where={"location.address": "str 1"}' + response, status = self.get(self.known_resource, where) + self.assert200(status) + + # test for nested resource field validating correctly + # (rows is list of dicts) + where = '?where={"rows.price": 10}' + response, status = self.get(self.known_resource, where) + self.assert200(status) + + # test for nested resource field validating correctly + # (dict_list_fixed_len is a fixed-size list of dicts) + where = '?where={"dict_list_fixed_len.key2": 1}' + response, status = self.get(self.known_resource, where) + self.assert200(status) + + # test for nested resource field not validating correctly + # (bad_base_key doesn't exist in the base resource schema) + where = '?where={"bad_base_key.sub": 1}' + response, status = self.get(self.known_resource, where) + self.assert400(status) + + # test for nested resource field not validating correctly + # (bad_sub_key doesn't exist in the dict_list_fixed_len schema) + where = '?where={"dict_list_fixed_len.bad_sub_key": 1}' + response, status = self.get(self.known_resource, where) + self.assert400(status) + def test_get_lookup_field_as_string(self): # Test that a resource where 'item_lookup_field' is set to a field # of string type and which value is castable to a ObjectId is still diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 818045c0a..6db81e77c 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -100,6 +100,19 @@ 'type': 'list', 'items': [{'type': 'objectid'}] }, + 'dict_list_fixed_len': { + 'type': 'list', + 'items': [ + { + 'type': 'dict', + 'schema': {'key1': {'type': 'string'}} + }, + { + 'type': 'dict', + 'schema': {'key2': {'type': 'integer'}} + } + ] + }, 'dependency_field1': { 'type': 'string', 'default': 'default' diff --git a/eve/utils.py b/eve/utils.py index ea9d185d6..b9a54f811 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -401,16 +401,52 @@ def validate_filter(filter): return r else: if config.VALIDATE_FILTERS: + def get_sub_schemas(base_schema): + def dict_sub_schema(base): + if base.get('type') == 'dict': + return base.get('schema') + + return None + + if base_schema.get('type') == 'list': + if 'schema' in base_schema: + # Try to get dict sub-schema for arbitrary sized list + sub = dict_sub_schema(base_schema['schema']) + return [sub] if sub is not None else [] + elif 'items' in base_schema: + # Try to get dict sub-schema(s) for fixed-size list + items = base_schema['items'] + sub_schemas = [] + for item in items: + sub = dict_sub_schema(item) + if sub is not None: + sub_schemas.append(sub) + + return sub_schemas + else: + sub = dict_sub_schema(base_schema) + return [sub] if sub is not None else [] + + def recursive_validate_filter(key, value, schema): + if key not in schema: + base_key, _, sub_keys = key.partition('.') + if sub_keys and base_key in schema: + # key is the composition of base field and sub-fields + for sub_schema in get_sub_schemas(schema[base_key]): + if recursive_validate_filter(sub_keys, value, sub_schema): + return True + + return False + else: + field_schema = schema.get(key) + v = Validator({key: field_schema}) + return v.validate({key: value}) + res_schema = config.DOMAIN[resource]['schema'] - if key not in res_schema: + if not recursive_validate_filter(key, value, res_schema): return "filter on '%s' is invalid" - else: - field_schema = res_schema.get(key) - v = Validator({key: field_schema}) - if not v.validate({key: value}): - return "filter on '%s' is invalid" - else: - return None + + return None if '*' in allowed and not config.VALIDATE_FILTERS: return None From bae675795d4a5fff8b707247707664e40d28e380 Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Fri, 23 Mar 2018 14:33:08 +0100 Subject: [PATCH 268/821] Fix ALLOWED_FILTERS behaviour for filters on sub-document fields --- eve/tests/methods/get.py | 22 ++++++++++++++++++++++ eve/utils.py | 18 ++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index f21e171af..043bd0482 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -535,6 +535,28 @@ def test_get_where_allowed_filters(self): '?where=%s' % where)) self.assert200(r.status_code) + # `allowed_filters` contains "rows" --> filter key "rows.price" must be allowed + self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ + ['rows'] + where = '{"rows.price": 10}' + r = self.test_client.get('%s%s' % (self.known_resource_url, + '?where=%s' % where)) + self.assert200(r.status_code) + + # `allowed_filters` contains "rows.price" --> filter key "rows.price" must be allowed + self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ + ['rows.price'] + r = self.test_client.get('%s%s' % (self.known_resource_url, + '?where=%s' % where)) + self.assert200(r.status_code) + + # `allowed_filters` contains "rows.price" --> filter key "rows" must NOT be allowed + where = '{"rows": {"sku": "value", "price": 10}}' + r = self.test_client.get('%s%s' % (self.known_resource_url, + '?where=%s' % where)) + self.assert400(r.status_code) + self.assertTrue(b"'rows' not allowed" in r.get_data()) + def test_get_with_post_override(self): # POST request with GET override turns into a GET headers = [('X-HTTP-Method-Override', 'GET')] diff --git a/eve/utils.py b/eve/utils.py index b9a54f811..158727519 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -386,8 +386,22 @@ def validate_filters(where, resource): def validate_filter(filter): for key, value in filter.items(): - if '*' not in allowed and key not in allowed: - return "filter on '%s' not allowed" % key + if '*' not in allowed: + def recursive_check_allowed(filter_key, allowed_filters): + # Filter key can be a plain key (e.g. "foo") or a dotted key (e.g. "dict.sub_dict.bar"). + # Starting from a dotted key, this function recursively checks `allowed_filters` for the key + # itself and for all its parent keys. + # This means that, for instance, "dict.sub_dict.bar" is an allowed filter key if `allowed_filters` + # contains any of "dict.sub_dict.bar", "dict.sub_dict" or "dict". + # Instead "dict" is an allowed filter key IFF `allowed_filters` contains "dict". + if filter_key not in allowed_filters: + base_composed_key, _, _ = filter_key.rpartition('.') + return base_composed_key and recursive_check_allowed(base_composed_key, allowed_filters) + + return True + + if not recursive_check_allowed(key, allowed): + return "filter on '%s' not allowed" % key if key in ('$or', '$and', '$nor'): if not isinstance(value, list): From d463a033f0f792c1c5f56c20dd4f5eea3d621179 Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Fri, 23 Mar 2018 16:59:19 +0100 Subject: [PATCH 269/821] Fix Flake8 errors --- eve/tests/methods/get.py | 9 ++++++--- eve/utils.py | 35 +++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 043bd0482..a3c8dd258 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -535,7 +535,8 @@ def test_get_where_allowed_filters(self): '?where=%s' % where)) self.assert200(r.status_code) - # `allowed_filters` contains "rows" --> filter key "rows.price" must be allowed + # `allowed_filters` contains "rows" --> filter key "rows.price" + # must be allowed self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ ['rows'] where = '{"rows.price": 10}' @@ -543,14 +544,16 @@ def test_get_where_allowed_filters(self): '?where=%s' % where)) self.assert200(r.status_code) - # `allowed_filters` contains "rows.price" --> filter key "rows.price" must be allowed + # `allowed_filters` contains "rows.price" --> filter key "rows.price" + # must be allowed self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ ['rows.price'] r = self.test_client.get('%s%s' % (self.known_resource_url, '?where=%s' % where)) self.assert200(r.status_code) - # `allowed_filters` contains "rows.price" --> filter key "rows" must NOT be allowed + # `allowed_filters` contains "rows.price" --> filter key "rows" + # must NOT be allowed where = '{"rows": {"sku": "value", "price": 10}}' r = self.test_client.get('%s%s' % (self.known_resource_url, '?where=%s' % where)) diff --git a/eve/utils.py b/eve/utils.py index 158727519..8e4843872 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -388,15 +388,20 @@ def validate_filter(filter): for key, value in filter.items(): if '*' not in allowed: def recursive_check_allowed(filter_key, allowed_filters): - # Filter key can be a plain key (e.g. "foo") or a dotted key (e.g. "dict.sub_dict.bar"). - # Starting from a dotted key, this function recursively checks `allowed_filters` for the key - # itself and for all its parent keys. - # This means that, for instance, "dict.sub_dict.bar" is an allowed filter key if `allowed_filters` - # contains any of "dict.sub_dict.bar", "dict.sub_dict" or "dict". - # Instead "dict" is an allowed filter key IFF `allowed_filters` contains "dict". + # Filter key can be a plain key (e.g. "foo") or a dotted + # key (e.g. "dict.sub_dict.bar"). + # Starting from a dotted key, this function recursively + # checks `allowed_filters` for the key itself and for all + # its parent keys. + # This means that, for instance, "dict.sub_dict.bar" is + # an allowed filter key if `allowed_filters` contains any + # of "dict.sub_dict.bar", "dict.sub_dict" or "dict". + # Instead "dict" is an allowed filter key IFF + # `allowed_filters` contains "dict". if filter_key not in allowed_filters: base_composed_key, _, _ = filter_key.rpartition('.') - return base_composed_key and recursive_check_allowed(base_composed_key, allowed_filters) + return base_composed_key and recursive_check_allowed( + base_composed_key, allowed_filters) return True @@ -424,11 +429,13 @@ def dict_sub_schema(base): if base_schema.get('type') == 'list': if 'schema' in base_schema: - # Try to get dict sub-schema for arbitrary sized list + # Try to get dict sub-schema for arbitrary + # sized list sub = dict_sub_schema(base_schema['schema']) return [sub] if sub is not None else [] elif 'items' in base_schema: - # Try to get dict sub-schema(s) for fixed-size list + # Try to get dict sub-schema(s) for + # fixed-size list items = base_schema['items'] sub_schemas = [] for item in items: @@ -445,9 +452,13 @@ def recursive_validate_filter(key, value, schema): if key not in schema: base_key, _, sub_keys = key.partition('.') if sub_keys and base_key in schema: - # key is the composition of base field and sub-fields - for sub_schema in get_sub_schemas(schema[base_key]): - if recursive_validate_filter(sub_keys, value, sub_schema): + # key is the composition of base field and + # sub-fields + sub_schemas = get_sub_schemas(schema[base_key]) + for sub_schema in sub_schemas: + if recursive_validate_filter(sub_keys, + value, + sub_schema): return True return False From d5659c20829a75c306190c049255d8b02c282c4f Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Mon, 26 Mar 2018 16:53:33 +0200 Subject: [PATCH 270/821] Update documentation for global 'ALLOWED_FILTERS' and resource-specific 'allowed_filters' settings --- docs/config.rst | 16 ++++++++++++++++ eve/utils.py | 10 ---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 504090fb8..b4df3b984 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -114,6 +114,14 @@ uppercase. ``/v1/``). Defaults to ``''``. ``ALLOWED_FILTERS`` List of fields on which filtering is allowed. + Entries in this list work in a hierarchical + way. This means that, for instance, filtering + on ``'dict.sub_dict.foo'`` is allowed if + ``ALLOWED_FILTERS`` contains any of + ``'dict.sub_dict.foo``, ``'dict.sub_dict'`` + or ``'dict'``. Instead filtering on + ``'dict'`` is allowed if ``ALLOWED_FILTERS`` + contains ``'dict'``. Can be set to ``[]`` (no filters allowed) or ``['*']`` (filters allowed on every field). Unless your API is comprised of @@ -798,6 +806,14 @@ always lowercase. :ref:`subresources`. ``allowed_filters`` List of fields on which filtering is allowed. + Entries in this list work in a hierarchical + way. This means that, for instance, filtering + on ``'dict.sub_dict.foo'`` is allowed if + ``allowed_filters`` contains any of + ``'dict.sub_dict.foo``, ``'dict.sub_dict'`` + or ``'dict'``. Instead filtering on + ``'dict'`` is allowed if ``allowed_filters`` + contains ``'dict'``. Can be set to ``[]`` (no filters allowed), or ``['*']`` (fields allowed on every field). Defaults to ``['*']``. diff --git a/eve/utils.py b/eve/utils.py index 8e4843872..215f808f7 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -388,16 +388,6 @@ def validate_filter(filter): for key, value in filter.items(): if '*' not in allowed: def recursive_check_allowed(filter_key, allowed_filters): - # Filter key can be a plain key (e.g. "foo") or a dotted - # key (e.g. "dict.sub_dict.bar"). - # Starting from a dotted key, this function recursively - # checks `allowed_filters` for the key itself and for all - # its parent keys. - # This means that, for instance, "dict.sub_dict.bar" is - # an allowed filter key if `allowed_filters` contains any - # of "dict.sub_dict.bar", "dict.sub_dict" or "dict". - # Instead "dict" is an allowed filter key IFF - # `allowed_filters` contains "dict". if filter_key not in allowed_filters: base_composed_key, _, _ = filter_key.rpartition('.') return base_composed_key and recursive_check_allowed( From fd1654873ff446c11dd607bcb4be16d8414a6910 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Mar 2018 10:12:26 +0200 Subject: [PATCH 271/821] Changelog for #1125. --- CHANGES | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 0dde81c1b..a8ef0828e 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: ``VALIDATE_FILTERS`` and ``ALLOWED_FILTERS`` do not work with + sub-document fields. Closes #1123 (Luca Moretto). - Fix documentation typos (Olof Johansson) - Fix a changelog typo (kreynen). - Update: bump Flask requirement to <=0.13. Closes #1111. @@ -20,12 +22,12 @@ Version 0.8 (Marcin Puhacz). - Change: ``JSON`` and ``XML`` settings are deprecated and will be removed in a future update. Use ``RENDERERS`` instead (Marcin Puhacz). -- New: Refactor index creation. We now have a new +- New: Refactor index creation. We now have a new ``eve.io.mongo.ensure_mongo_indexes()`` function which ensures that eventual - ``mongo_indexes`` defined for a resource are created on the active - database. The function can be imported and invoked, for example in - multi-db workflows where a db is activated based on the - authenticated user performing the request (via custom auth classes). + ``mongo_indexes`` defined for a resource are created on the active database. + The function can be imported and invoked, for example in multi-db workflows + where a db is activated based on the authenticated user performing the + request (via custom auth classes). - Fix: add sphinxcontrib-embedly to dev-requirements.txt. - New: when the media endpoint is enabled, the default authentication class will be used to secure it. Closes #1083. From 5a878c23f7e9df2e64271fe781778d73846cd8d8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Mar 2018 11:11:36 +0200 Subject: [PATCH 272/821] Reinforce notion that mongod must be running. Closes #891. --- docs/quickstart.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c67b0714f..11246482c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,9 +3,10 @@ Quickstart ========== -Eager to get started? This page gives a good introduction to Eve. It -assumes that: +Eager to get started? This page gives a first introduction to Eve. +Prerequisites +------------- - You already have Eve installed. If you do not, head over to the :ref:`install` section. - MongoDB is installed_. @@ -95,7 +96,9 @@ Try requesting ``people`` now: This time we also got an ``_items`` list. The ``_links`` are relative to the resource being accessed, so you get a link to the parent resource (the home -page) and to the resource itself. +page) and to the resource itself. If you got a timeout error from pymongo, make +sure the prerequistes are met. Chances are that the ``mongod`` server process +is not runnig. By default Eve APIs are read-only: From 8b6bbcc797f0b8096cb8a0aff17be65782a3d6cd Mon Sep 17 00:00:00 2001 From: Artem Kolesnikov Date: Mon, 26 Mar 2018 23:54:34 +1100 Subject: [PATCH 273/821] Added support for Mongo 3.2+ params $caseSensitive and $diacriticSensitive in $text operator. --- eve/io/mongo/mongo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index a840987ce..4257479ff 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -102,8 +102,8 @@ class Mongo(DataLayer): ['$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin'] + ['$or', '$and', '$not', '$nor'] + ['$mod', '$regex', '$text', '$where'] + - ['$options', '$search', '$language'] + - ['$exists', '$type'] + + ['$options', '$search', '$language', '$caseSensitive'] + + ['$diacriticSensitive', '$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + ['$geometry', '$maxDistance'] + ['$all', '$elemMatch', '$size'] + From 713ee5c660f2e780835d6c93f6e2e19d560f1676 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 28 Mar 2018 09:26:32 +0200 Subject: [PATCH 274/821] Changelog for #1126 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index a8ef0828e..be532438e 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: Add support for mongo's ``$caseSensitive`` and ``$diactricSensitive`` + query operators (Artem Kolesnikov). - Fix: ``VALIDATE_FILTERS`` and ``ALLOWED_FILTERS`` do not work with sub-document fields. Closes #1123 (Luca Moretto). - Fix documentation typos (Olof Johansson) From 316827d67c883a8531a31c1af3480595abcc4155 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 28 Mar 2018 10:37:07 +0200 Subject: [PATCH 275/821] Pin testfixtures to v5.x on py26. Testfixtures v6.0+ fails on Python 2.6. Addresses #1128. --- .vscode/launch.json | 186 ++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 8 ++ CHANGES | 2 + setup.py | 1 + 4 files changed, 197 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..65608537d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,186 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "${workspaceFolder}", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Python: Attach", + "type": "python", + "request": "attach", + "localRoot": "${workspaceFolder}", + "remoteRoot": "${workspaceFolder}", + "port": 3000, + "secret": "my_secret", + "host": "localhost" + }, + { + "name": "Python: Terminal (integrated)", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [] + }, + { + "name": "Python: Terminal (external)", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [] + }, + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceFolder}/manage.py", + "cwd": "${workspaceFolder}", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput", + "DjangoDebugging" + ] + }, + { + "name": "Python: Flask (0.11.x or later)", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", + "cwd": "${workspaceFolder}", + "env": { + "FLASK_APP": "${workspaceFolder}/quickstart/app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Python: Flask (0.10.x or earlier)", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceFolder}/run.py", + "cwd": "${workspaceFolder}", + "args": [], + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Python: PySpark", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "osx": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "windows": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit.cmd" + }, + "linux": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "program": "${file}", + "cwd": "${workspaceFolder}", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "module": "module.name", + "cwd": "${workspaceFolder}", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Python: Pyramid", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "cwd": "${workspaceFolder}", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "args": [ + "${workspaceFolder}/development.ini" + ], + "debugOptions": [ + "RedirectOutput", + "Pyramid" + ] + }, + { + "name": "Python: Watson", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceFolder}/console.py", + "cwd": "${workspaceFolder}", + "args": [ + "dev", + "runserver", + "--noreload=True" + ], + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [ + "RedirectOutput" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a9c43166f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.pythonPath": "/Users/nicola/.virtualenvs/eve/bin/python3", + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + "--ignore=E731,E722,F821", + ], + "python.linting.pylintEnabled": false, +} \ No newline at end of file diff --git a/CHANGES b/CHANGES index be532438e..26e240b7b 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Dev: pin testfixtures to v5.x as latest releases break on Python 2.6. + Addresses #1128. - New: Add support for mongo's ``$caseSensitive`` and ``$diactricSensitive`` query operators (Artem Kolesnikov). - Fix: ``VALIDATE_FILTERS`` and ``ALLOWED_FILTERS`` do not work with diff --git a/setup.py b/setup.py index a26d977c5..04ff3ff01 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ # Python 2.6 install_requires.append('backport_collections') install_requires.append('importlib==1.0.4') + install_requires.append('testfixtures<6.0.0') setup( From 9864600482c446fc4d7b523d1cf3d3fe36cbfa55 Mon Sep 17 00:00:00 2001 From: DHuan Date: Tue, 27 Mar 2018 03:31:03 +0800 Subject: [PATCH 276/821] Allow client projection with static exclusive projection Fix #1036 : allow client projection with static exclusion Changes: - Enable client projection with static exclusive projection - Enhance tests and docs accordingly Projection behaviors: - Static projection setting will allow client projection - Static inclusive projection will block sniffing - Static exclusive projection will allow sniffing - Quite weird but backwards compatible... This PR try to patch-fix the issue but not mess up with existing code. However, current code may require some refactoring. - NoneType projections are quite annoying and hard to maintain. Should they always be dictionaries and converted to None later (for flask-pymongo)? --- AUTHORS | 2 + docs/config.rst | 43 ++++++++++++--- eve/flaskapp.py | 34 +++++++----- eve/io/base.py | 8 ++- eve/tests/__init__.py | 8 +++ eve/tests/io/media.py | 51 ++++++++++++++++++ eve/tests/methods/get.py | 106 +++++++++++++++++++++++++++++++++++++ eve/tests/test_settings.py | 12 +++++ 8 files changed, 242 insertions(+), 22 deletions(-) diff --git a/AUTHORS b/AUTHORS index 3598fe274..95fe73a76 100644 --- a/AUTHORS +++ b/AUTHORS @@ -167,3 +167,5 @@ Patches and Contributions - kreynen - mmizotin - xgdgsc +- Hugo Larcher +- Huan Di \ No newline at end of file diff --git a/docs/config.rst b/docs/config.rst index b4df3b984..135955304 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -489,11 +489,11 @@ uppercase. ``JSON_SORT_KEYS`` ``True`` to enable JSON key sorting, ``False`` otherwise. Defaults to ``False``. -``JSON_REQUEST_CONTENT_TYPES`` Supported JSON content types. Useful when +``JSON_REQUEST_CONTENT_TYPES`` Supported JSON content types. Useful when you need support for vendor-specific json types. Please note: responses will still carry the standard ``application/json`` - type. Defaults to ``['application/json']``. + type. Defaults to ``['application/json']``. ``VALIDATION_ERROR_STATUS`` The HTTP status code to use for validation errors. Defaults to ``422``. @@ -1337,13 +1337,13 @@ defining the field validation rules. Allowed validation rules are: set in the list. See `*of-rules `_ in Cerberus docs. ``allof`` Same as ``anyof``, except that all rule - collections in the list must validate. + collections in the list must validate. ``noneof`` Same as ``anyof``, except that it requires no rule collections in the list to validate. ``oneof`` Same as ``anyof``, except that only one rule - collections in the list can validate. + collections in the list can validate. ``coerce`` Type coercion allows you to apply a callable to a value before any other validators run. The @@ -1493,6 +1493,14 @@ By default API responses to GET requests will include all fields defined by the corresponding resource schema_. The ``projection`` setting of the `datasource` resource keyword allows you to redefine the fieldset. +When you want to hide some *secret fields* from client, you should use +inclusive projection setting and include all fields should be exposed. While, +when you want to limit default responsesto certain fields but still allow them +to be accessible through client-side projections, you should use exclusive +projection setting and exclude fields should be omitted. + +The following is an example for inclusive projection setting: + :: people = { @@ -1502,9 +1510,18 @@ resource keyword allows you to redefine the fieldset. } The above setting will expose only the `username` field to GET requests, no -matter the schema_ defined for the resource. +matter the schema_ defined for the resource. And other fields **will not** be +exposed even by client-side projection. The following API call will not return +`lastname` or `born`. + +.. code-block:: console + + $ curl -i http://eve-demo.herokuapp.com/people?projection={"lastname": 1, "born": 1} + HTTP/1.1 200 OK -Likewise, you can exclude fields from API responses: +You can also exclude fields from API responses. But this time, the excluded +fields **will be** exposed to client-side projection. The following is an +example for exclusive projection setting: :: @@ -1514,7 +1531,19 @@ Likewise, you can exclude fields from API responses: } } -The above will include all document fields but `username`. +The above will include all document fields but `username`. However, the +following API call will return `username` this time. Thus, you can exploit this +behaviour to serve media fields or other expensive fields. + +In most cases, none or inclusive projection setting is more preferred. With +inclusive projection, secret fields are taken care from server side, and default +fields returned can be defined by short-cut functions from client-side. + +.. code-block:: console + + $ curl -i http://eve-demo.herokuapp.com/people?projection={"username": 1} + HTTP/1.1 200 OK + Please note that POST and PATCH methods will still allow the whole schema to be manipulated. This feature can come in handy when, for example, you want to diff --git a/eve/flaskapp.py b/eve/flaskapp.py index cead3f9d2..7fd1a950f 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -666,7 +666,6 @@ def _set_resource_datasource(self, resource, schema, settings): ds.setdefault('default_sort', None) self._set_resource_projection(ds, schema, settings) - aggregation = ds.setdefault('aggregation', None) if aggregation: aggregation.setdefault('options', {}) @@ -688,18 +687,27 @@ def _set_resource_projection(self, ds, schema, settings): projection = ds.get('projection', {}) - # check if any exclusion projection is defined - exclusion = any(((k, v) for k, v in projection.items() if v == 0)) \ - if projection else None - - # If no exclusion projection is defined, enhance the projection - # with automatic fields. Using both inclusion and exclusion will - # be rejected by Mongo - if not exclusion and len(schema) and \ - settings['allow_unknown'] is False: + # If exclusion projections are defined, they are use for + # concealing fields (rather than actual mongo exlusions). + # If inclusion projections are defined, exclusion projections are + # just ignored. + # Enhance the projection with automatic fields. + if len(schema) and settings['allow_unknown'] is False: + exclusion_projection = dict([(k, v) for k, v in projection.items() + if v == 0]) + inclusion_projection = dict([(k, v) for k, v in projection.items() + if v == 1]) + if exclusion_projection or inclusion_projection: + projection = inclusion_projection + ds['projection'] = projection + # use all fields not excluded if not projection: - projection.update(dict((field, 1) for (field) in schema)) - + projection.update( + dict((field, 1) for (field) in schema + if field not in exclusion_projection)) + # add back exclusion projection, and deal with them together with + # client projection later in 'io/base.py' + projection.update(exclusion_projection) # enable retrieval of actual schema fields only. Eventual db # fields not included in the schema won't be returned. # despite projection, automatic fields are always included. @@ -717,7 +725,7 @@ def _set_resource_projection(self, ds, schema, settings): projection = None ds.setdefault('projection', projection) - if settings['soft_delete'] is True and not exclusion and \ + if settings['soft_delete'] is True and \ ds['projection'] is not None: ds['projection'][self.config['DELETED']] = 1 diff --git a/eve/io/base.py b/eve/io/base.py index 6df507c2b..c345e0f8d 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -392,7 +392,6 @@ def _datasource_ex(self, resource, query=None, client_projection=None, """ datasource, filter_, projection_, sort_ = self.datasource(resource) - if client_sort: sort = client_sort else: @@ -439,7 +438,12 @@ def _datasource_ex(self, resource, query=None, client_projection=None, # there's no standard projection so we assume we are in a # allow_unknown = True fields = client_projection - + elif fields is not None: + # drop exclusion projection + fields = dict([(field, 1) for field, value in fields.items() if + value]) + # if fields == {}: + # fields = None # If the current HTTP method is in `public_methods` or # `public_item_methods`, skip the `auth_field` check diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 857c84f63..e3bdd6d3c 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -381,6 +381,14 @@ def setUp(self, url_converters=None): self.domain[ self.different_resource]['url']) + self.different_resource_exclude = 'contacts_hide_born' + self.different_resource_exclude_url = ( + '/%s' % self.domain[self.different_resource_exclude]['url']) + + self.resource_exclude_media = 'contacts_hide_media' + self.resource_exclude_media_url = ( + '/%s' % self.domain[self.resource_exclude_media]['url']) + response, _ = self.get('contacts', '?max_results=2') contact = self.response_item(response) self.item = contact diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index b67e13456..29c441fed 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -275,6 +275,50 @@ def test_gridfs_media_storage_delete(self): _id))) self.assert404(s) + def test_get_media_can_leverage_projection(self): + """ Test that static projection expose fields other than media + and client projection on media will work. + """ + # post a document with *hiding media* + r, s = self._post_hide_media() + + _id = r[self.id_field] + + projection = '{"media": 1}' + response, status = self.parse_response(self.test_client.get( + '%s/%s?projection=%s' % + (self.resource_exclude_media_url, _id, projection)) + ) + self.assert200(status) + + self.assertFalse('title' in response) + self.assertFalse('ref' in response) + # client-side projection should work + self.assertTrue('media' in response) + self.assertTrue(self.domain[self.known_resource]['id_field'] + in response) + self.assertTrue(self.app.config['ETAG'] in response) + self.assertTrue(self.app.config['LAST_UPDATED'] in response) + self.assertTrue(self.app.config['DATE_CREATED'] in response) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + + response, status = self.parse_response(self.test_client.get( + '%s/%s' % (self.resource_exclude_media_url, _id))) + self.assert200(status) + + self.assertTrue('title' in response) + self.assertTrue('ref' in response) + # not shown without projection + self.assertFalse('media' in response) + self.assertTrue(self.domain[self.known_resource]['id_field'] + in response) + self.assertTrue(self.app.config['ETAG'] in response) + self.assertTrue(self.app.config['LAST_UPDATED'] in response) + self.assertTrue(self.app.config['DATE_CREATED'] in response) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + def test_gridfs_media_storage_delete_projection(self): """ test that #284 is fixed: If you have a media field, and set datasource projection to 0 for that field, the media will not be @@ -396,3 +440,10 @@ def _post(self): self.test_value} return self.parse_response(self.test_client.post( self.url, data=data, headers=self.headers)) + + def _post_hide_media(self): + # send a file and a required, ordinary field with no issues + data = {'media': (BytesIO(self.clean), 'test.txt'), self.test_field: + self.test_value} + return self.parse_response(self.test_client.post( + self.resource_exclude_media_url, data=data, headers=self.headers)) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index a3c8dd258..149a871c7 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -349,6 +349,112 @@ def test_get_static_projection(self): self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + def test_get_server_include_projection_can_exclude(self): + """ Test that static projection only expose fields included + and support client projection on these fields. + """ + # exclude `ref` by client side + projection = '{"ref": 0}' + response, status = self.get(self.different_resource, + '?projection=%s' % + projection) + self.assert200(status) + + resource = response['_items'] + + # 'users' has a static inclusive projection with 'username' and 'ref' + # fields, so other document fields should be excluded. + # and client can further exclude 'ref' or 'username'. + for r in resource: + self.assertFalse('location' in r) + self.assertFalse('role' in r) + self.assertFalse('prog' in r) + self.assertTrue('username' in r) + self.assertFalse('ref' in r) + self.assertTrue(self.domain[self.known_resource]['id_field'] in r) + self.assertTrue(self.app.config['ETAG'] in r) + self.assertTrue(self.app.config['LAST_UPDATED'] in r) + self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + + def test_get_server_include_projection_block_sniff(self): + """ Test that static projection only expose fields included + and client projection on other fields will fail. + """ + # shouldn't work when including `prog` (excluded) by client side + projection = '{"prog": 1}' + response, status = self.get(self.different_resource, + '?projection=%s' % + projection) + self.assert200(status) + + resource = response['_items'] + for r in resource: + self.assertFalse('location' in r) + self.assertFalse('role' in r) + # shouldn't work + self.assertFalse('prog' in r) + self.assertFalse('username' in r) + self.assertFalse('ref' in r) + self.assertTrue(self.domain[self.known_resource]['id_field'] in r) + self.assertTrue(self.app.config['ETAG'] in r) + self.assertTrue(self.app.config['LAST_UPDATED'] in r) + self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + + def test_get_server_exclude_projection_can_project_others(self): + """ Test that static projection expose fields other than excluded + and support client projection on exposed fields. + """ + projection = '{"prog": 1, "location":1}' + response, status = self.get(self.different_resource_exclude, + '?projection=%s' % + projection) + self.assert200(status) + + resource = response['_items'] + + # 'users' has a static inclusive projection with 'username' and 'ref' + # fields, so other document fields should be excluded. + # and client can further exclude 'ref' or 'username'. + for r in resource: + self.assertTrue('location' in r) + self.assertFalse('role' in r) + self.assertTrue('prog' in r) + self.assertFalse('born' in r) + self.assertTrue(self.domain[self.known_resource]['id_field'] in r) + self.assertTrue(self.app.config['ETAG'] in r) + self.assertTrue(self.app.config['LAST_UPDATED'] in r) + self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + + def test_get_server_exlcude_projection_can_sniff(self): + """ Test that static projection expose fields other than excluded + and client projection on excluded **will work**. + """ + projection = '{"born": 1}' + response, status = self.get(self.different_resource_exclude, + '?projection=%s' % + projection) + self.assert200(status) + + resource = response['_items'] + for r in resource: + self.assertFalse('location' in r) + self.assertFalse('role' in r) + self.assertFalse('prog' in r) + # should work + self.assertTrue('born' in r) + self.assertTrue(self.domain[self.known_resource]['id_field'] in r) + self.assertTrue(self.app.config['ETAG'] in r) + self.assertTrue(self.app.config['LAST_UPDATED'] in r) + self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) + self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + def test_get_custom_projection(self): self.app.config['QUERY_PROJECTION'] = 'view' projection = '{"prog": 1}' diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 6db81e77c..bdff54511 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -182,6 +182,16 @@ users['item_title'] = 'user' users['additional_lookup']['field'] = 'username' +contacts_hide_born = copy.deepcopy(contacts) +contacts_hide_born['url'] = 'contacts/hide_born' +contacts_hide_born['datasource']['source'] = 'contacts' +contacts_hide_born['datasource']['projection'] = {'born': 0} + +contacts_hide_media = copy.deepcopy(contacts) +contacts_hide_media['url'] = 'contacts/hide_media' +contacts_hide_media['datasource']['source'] = 'contacts' +contacts_hide_media['datasource']['projection'] = {'media': 0, 'born': 0} + invoices = { 'schema': { 'inv_number': {'type': 'string'}, @@ -339,6 +349,8 @@ 'contacts': contacts, 'users': users, 'users_overseas': users_overseas, + 'contacts_hide_born': contacts_hide_born, + 'contacts_hide_media': contacts_hide_media, 'invoices': invoices, 'versioned_invoices': versioned_invoices, 'required_invoices': required_invoices, From b76118d05b1759c9e432595de9bf37689a9b2a63 Mon Sep 17 00:00:00 2001 From: DHuan Date: Wed, 28 Mar 2018 15:25:21 +0800 Subject: [PATCH 277/821] refactor : staic projection is always dictionary --- eve/flaskapp.py | 28 +++++++++++----------------- eve/io/base.py | 15 ++++++--------- eve/io/mongo/mongo.py | 12 +++++++----- eve/tests/methods/post.py | 2 +- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 7fd1a950f..dca657455 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -684,7 +684,7 @@ def _set_resource_projection(self, ds, schema, settings): .. versionadded:: 0.6.2 """ - + # get existing or empty projection setting projection = ds.get('projection', {}) # If exclusion projections are defined, they are use for @@ -693,21 +693,15 @@ def _set_resource_projection(self, ds, schema, settings): # just ignored. # Enhance the projection with automatic fields. if len(schema) and settings['allow_unknown'] is False: - exclusion_projection = dict([(k, v) for k, v in projection.items() - if v == 0]) inclusion_projection = dict([(k, v) for k, v in projection.items() if v == 1]) - if exclusion_projection or inclusion_projection: - projection = inclusion_projection - ds['projection'] = projection - # use all fields not excluded - if not projection: + exclusion_projection = dict([(k, v) for k, v in projection.items() + if v == 0]) + # if inclusion project is empty, add all fields not excluded + if not inclusion_projection: projection.update( dict((field, 1) for (field) in schema if field not in exclusion_projection)) - # add back exclusion projection, and deal with them together with - # client projection later in 'io/base.py' - projection.update(exclusion_projection) # enable retrieval of actual schema fields only. Eventual db # fields not included in the schema won't be returned. # despite projection, automatic fields are always included. @@ -720,14 +714,14 @@ def _set_resource_projection(self, ds, schema, settings): projection[ settings['id_field'] + self.config['VERSION_ID_SUFFIX']] = 1 - else: - # all fields are returned. - projection = None + ds.setdefault('projection', projection) - if settings['soft_delete'] is True and \ - ds['projection'] is not None: - ds['projection'][self.config['DELETED']] = 1 + if settings['soft_delete'] is True and projection: + projection[self.config['DELETED']] = 1 + + # set projection and projection is always a dictionary + ds['projection'] = projection # list of all media fields for the resource if isinstance(schema, dict): diff --git a/eve/io/base.py b/eve/io/base.py index c345e0f8d..f44bdf2bf 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -423,7 +423,7 @@ def _datasource_ex(self, resource, query=None, client_projection=None, # projection for the resource (avoid sniffing of private # fields) keep_fields = auto_fields(resource) - if 0 not in client_projection.values(): + if 1 in client_projection.values(): # inclusive projection - all values are 0 unless spec. or # auto fields = dict([(field, field in keep_fields) for field in @@ -432,18 +432,15 @@ def _datasource_ex(self, resource, query=None, client_projection=None, field_base = field.split('.')[0] if field_base not in keep_fields and field_base in fields: fields[field] = value - fields = dict([(field, 1) for field, value in fields.items() if - value]) else: # there's no standard projection so we assume we are in a # allow_unknown = True fields = client_projection - elif fields is not None: - # drop exclusion projection - fields = dict([(field, 1) for field, value in fields.items() if - value]) - # if fields == {}: - # fields = None + # always drop exclusion projection, thus avoid mixed projection not + # supported by db driver + fields = dict([(field, 1) for field, value in fields.items() if + value]) + # If the current HTTP method is in `public_methods` or # `public_item_methods`, skip the `auth_field` check diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 4257479ff..c0bd4a7a8 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -272,7 +272,7 @@ def find(self, resource, req, sub_resource_lookup): if sort is not None: args['sort'] = sort - if projection is not None: + if projection: args['projection'] = projection return self.pymongo(resource).db[datasource].find(**args) @@ -319,9 +319,9 @@ def find_one(self, resource, req, **lookup): (not self.query_contains_field(lookup, config.DELETED)): filter_ = self.combine_queries( filter_, {config.DELETED: {"$ne": True}}) - + # Here, we feed pymongo with `None` if projection is empty. return self.pymongo(resource).db[datasource] \ - .find_one(filter_, projection) + .find_one(filter_, projection or None) def find_one_raw(self, resource, **lookup): """ Retrieves a single raw document. @@ -387,9 +387,11 @@ def find_list_of_ids(self, resource, ids, client_projection=None): datasource, spec, projection, _ = self._datasource_ex( resource, query=query, client_projection=client_projection ) - + # projection of {} return all fields in MongoDB, but + # pymongo will only return `_id`. It's a design flaw upstream. + # Here, we feed pymongo with `None` if projection is empty. documents = self.pymongo(resource).db[datasource].find( - filter=spec, projection=projection + filter=spec, projection=(projection or None) ) return documents diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 686b7d787..30d3d4a50 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -416,7 +416,7 @@ def test_post_allow_unknown(self): # don't have to re-initialize the whole app.) settings = self.app.config['DOMAIN'][self.known_resource] settings['allow_unknown'] = True - settings['datasource']['projection'] = None + settings['datasource']['projection'] = {} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From 7c4f4f99473b634cafc28dbf0bbcc6b3124d846f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 28 Mar 2018 10:50:15 +0200 Subject: [PATCH 278/821] Changelog for #1128 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 26e240b7b..f9bb9808a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: Cannot define default projection and request specific field. Closes + #1036 (DHuan). - Dev: pin testfixtures to v5.x as latest releases break on Python 2.6. Addresses #1128. - New: Add support for mongo's ``$caseSensitive`` and ``$diactricSensitive`` From 9dabbf5a3b60a7d109b62ce2f92daf3a8c98e4f0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 28 Mar 2018 10:50:40 +0200 Subject: [PATCH 279/821] DHuan --- AUTHORS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 95fe73a76..eb4bd2ec3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,6 +30,7 @@ Patches and Contributions - Conrad Burchert - Cyprien Pannier - Cyril Bonnard +- DHuan - Daniel Lytkin - Daniele Pizzolli - Danse @@ -61,6 +62,8 @@ Patches and Contributions - Harro van der Klauw - Hasan Pekdemir - Henrique Barroso +- Huan Di +- Hugo Larcher - James Stewart - Jaroslav Semančík - Javier Gonel @@ -167,5 +170,3 @@ Patches and Contributions - kreynen - mmizotin - xgdgsc -- Hugo Larcher -- Huan Di \ No newline at end of file From 9a2350b7eb39b82d230c94c9cf0909968c6e5e7f Mon Sep 17 00:00:00 2001 From: Hung Le Date: Sun, 24 Dec 2017 16:08:54 +0700 Subject: [PATCH 280/821] Oplog skipped even if confg.OPLOG=True Check for resource name validity is being reversed. Closes #1074. --- eve/methods/common.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 2ffb666a9..036fd8020 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1234,7 +1234,7 @@ def oplog_push(resource, document, op, id=None): 'r' = resource endpoint, 'o' = operation performed, 'i' = unique id of the document involved, - 'pi' = client IP, + 'ip' = client IP, 'c' = changes config.LAST_UPDATED, config.LAST_CREATED and AUTH_FIELD are not being @@ -1258,9 +1258,10 @@ def oplog_push(resource, document, op, id=None): .. versionadded:: 0.5 """ + if not config.OPLOG \ or op not in config.OPLOG_METHODS\ - or resource in config.URLS[resource]: + or resource not in config.URLS: return resource_def = config.DOMAIN[resource] From eed600260484b7cc638cc0c47a58ef44bc8488dd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Mar 2018 10:51:08 +0200 Subject: [PATCH 281/821] Regression test for PR #1095. The original test was a false positive. --- eve/tests/methods/common.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 16497d528..09954e1d2 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -653,10 +653,12 @@ def oplog_callback(resource, entries): self.assertTrue('customvalue' in oplog_entry['extra']['customfield']) def test_post_oplog(self): - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + r = self.test_client.post( + self.different_resource_url, + data=json.dumps({'username': 'test', 'ref': + '1234567890123456789012345' }), + headers=self.headers, environ_base={'REMOTE_ADDR': '127.0.0.1'}) + r, status = self.oplog_get() self.assert200(status) self.assertEqual(len(r['_items']), 1) From 795abe9154448468c3036dc80e5b8a9ac0407eb3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Mar 2018 10:53:36 +0200 Subject: [PATCH 282/821] Changelog for #1095. --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index f9bb9808a..21f13870d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: OPLOG skipped even if ``OPLOG = True``. Closes 1074 (Hung Le). - Fix: Cannot define default projection and request specific field. Closes #1036 (DHuan). - Dev: pin testfixtures to v5.x as latest releases break on Python 2.6. From 91edb11c2d2112c5f8d5468b77b8f83ff4d6ef66 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Mar 2018 10:54:20 +0200 Subject: [PATCH 283/821] Hung Le --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index eb4bd2ec3..26c4c3917 100644 --- a/AUTHORS +++ b/AUTHORS @@ -64,6 +64,7 @@ Patches and Contributions - Henrique Barroso - Huan Di - Hugo Larcher +- Hung Le - James Stewart - Jaroslav Semančík - Javier Gonel From 194bc732fa2aa48797e0f8c37a4b5b199ebddab0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Mar 2018 11:03:00 +0200 Subject: [PATCH 284/821] typo --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 21f13870d..d8f3a0997 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ -- Fix: OPLOG skipped even if ``OPLOG = True``. Closes 1074 (Hung Le). +- Fix: OPLOG skipped even if ``OPLOG = True``. Closes #1074 (Hung Le). - Fix: Cannot define default projection and request specific field. Closes #1036 (DHuan). - Dev: pin testfixtures to v5.x as latest releases break on Python 2.6. From 6eb74c5f4356712ac024c452db8d25fa9eb6f124 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Mar 2018 11:08:28 +0200 Subject: [PATCH 285/821] flake8 --- eve/tests/methods/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 09954e1d2..471aa33a3 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -656,7 +656,7 @@ def test_post_oplog(self): r = self.test_client.post( self.different_resource_url, data=json.dumps({'username': 'test', 'ref': - '1234567890123456789012345' }), + '1234567890123456789012345'}), headers=self.headers, environ_base={'REMOTE_ADDR': '127.0.0.1'}) r, status = self.oplog_get() From d7adbe7f73b2852f8db86f066c521bd10fc3213d Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Fri, 30 Mar 2018 10:37:05 +0200 Subject: [PATCH 286/821] Fix PUT behavior with User-Restricted Resource Access --- eve/io/base.py | 69 ++++++++++++++++++++++++++----------------- eve/io/mongo/mongo.py | 7 +++-- eve/methods/common.py | 16 ++++++++-- eve/methods/put.py | 16 ++++++++-- eve/tests/auth.py | 40 +++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 33 deletions(-) diff --git a/eve/io/base.py b/eve/io/base.py index f44bdf2bf..a9a06731a 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -151,7 +151,8 @@ def aggregate(self, resource, pipeline, options): """ raise NotImplementedError - def find_one(self, resource, req, **lookup): + def find_one(self, resource, req, check_auth_value=True, + force_auth_field_projection=False, **lookup): """ Retrieves a single document/record. Consumed when a request hits an item endpoint (`/people/id/`). @@ -164,6 +165,14 @@ def find_one(self, resource, req, **lookup): etc). As we are going to only look for one document here, the only req attribute that you want to process here is ``req.projection``. + :param check_auth_value: a boolean flag indicating if the find + operation should consider user-restricted + resource access. Defaults to ``True``. + :param force_auth_field_projection: a boolean flag indicating if the + find operation should always + include the user-restricted + resource access field (if + configured). Defaults to ``False``. :param **lookup: the lookup fields. This will most likely be a record id or, if alternate lookup is supported by the API, @@ -343,7 +352,8 @@ def datasource(self, resource): return source, filter_, projection, sort, def _datasource_ex(self, resource, query=None, client_projection=None, - client_sort=None): + client_sort=None, check_auth_value=True, + force_auth_field_projection=False): """ Returns both db collection and exact query (base filter included) to which an API resource refers to. @@ -446,35 +456,40 @@ def _datasource_ex(self, resource, query=None, client_projection=None, # Only inject the auth_field in the query when not creating new # documents. - if request and request.method not in ('POST', 'PUT'): + if request and request.method != 'POST' and ( + check_auth_value or force_auth_field_projection + ): auth_field, request_auth_value = auth_field_and_value(resource) - if auth_field and request_auth_value: - if query: - # If the auth_field *replaces* a field in the query, - # and the values are /different/, deny the request - # This prevents the auth_field condition from - # overwriting the query (issue #77) - auth_field_in_query = \ - self.app.data.query_contains_field(query, auth_field) - if auth_field_in_query and \ + if auth_field: + if request_auth_value and check_auth_value: + if query: + # If the auth_field *replaces* a field in the query, + # and the values are /different/, deny the request + # This prevents the auth_field condition from + # overwriting the query (issue #77) + auth_field_in_query = \ + self.app.data.query_contains_field(query, + auth_field) + if auth_field_in_query and \ self.app.data.get_value_from_query( query, auth_field) != request_auth_value: - abort(401, description='Incompatible User-Restricted ' - 'Resource request. ' - 'Request was for "%s"="%s" but `auth_field` ' - 'requires "%s"="%s".' % ( - auth_field, - self.app.data.get_value_from_query( - query, auth_field), - auth_field, - request_auth_value) - ) + desc = 'Incompatible User-Restricted Resource ' \ + 'request. Request was for "%s"="%s" but ' \ + '`auth_field` requires "%s"="%s".' % ( + auth_field, + self.app.data.get_value_from_query( + query, auth_field), + auth_field, + request_auth_value) + abort(401, description=desc) + else: + query = self.app.data.combine_queries( + query, {auth_field: request_auth_value} + ) else: - query = self.app.data.combine_queries( - query, {auth_field: request_auth_value} - ) - else: - query = {auth_field: request_auth_value} + query = {auth_field: request_auth_value} + if force_auth_field_projection: + fields[auth_field] = 1 return datasource, query, fields, sort def _client_projection(self, req): diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index c0bd4a7a8..828e767ee 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -277,7 +277,8 @@ def find(self, resource, req, sub_resource_lookup): return self.pymongo(resource).db[datasource].find(**args) - def find_one(self, resource, req, **lookup): + def find_one(self, resource, req, check_auth_value=True, + force_auth_field_projection=False, **lookup): """ Retrieves a single document. :param resource: resource name. @@ -312,7 +313,9 @@ def find_one(self, resource, req, **lookup): datasource, filter_, projection, _ = self._datasource_ex( resource, lookup, - client_projection) + client_projection, + check_auth_value=check_auth_value, + force_auth_field_projection=force_auth_field_projection) if (config.DOMAIN[resource]['soft_delete']) and \ (not req or not req.show_deleted) and \ diff --git a/eve/methods/common.py b/eve/methods/common.py index 036fd8020..489f81d86 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -34,7 +34,9 @@ from backport_collections import Counter -def get_document(resource, concurrency_check, original=None, **lookup): +def get_document(resource, concurrency_check, original=None, + check_auth_value=True, force_auth_field_projection=False, + **lookup): """ Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client request references the current representation of the document before @@ -45,6 +47,14 @@ def get_document(resource, concurrency_check, original=None, **lookup): :param resource: the name of the resource to which the document belongs to. :param concurrency_check: boolean check for concurrency control :param original: in case the document was already retrieved before + :param check_auth_value: a boolean flag indicating if the find operation + should consider user-restricted resource + access. Defaults to ``True``. + :param force_auth_field_projection: a boolean flag indicating if the + find operation should always include + the user-restricted resource access + field (if configured). Defaults to + ``False``. :param **lookup: document lookup query .. versionchanged:: 0.6 @@ -70,7 +80,9 @@ def get_document(resource, concurrency_check, original=None, **lookup): if original: document = original else: - document = app.data.find_one(resource, req, **lookup) + document = app.data.find_one(resource, req, check_auth_value, + force_auth_field_projection, + **lookup) if document: e_if_m = config.ENFORCE_IF_MATCH diff --git a/eve/methods/put.py b/eve/methods/put.py index d07dff549..e57dbdc55 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -14,7 +14,7 @@ from flask import current_app as app, abort from werkzeug import exceptions -from eve.auth import requires_auth +from eve.auth import auth_field_and_value, requires_auth from eve.methods.common import get_document, parse, payload as payload_, \ ratelimit, pre_event, store_media_files, resolve_user_restricted_access, \ resolve_embedded_fields, build_response_document, marshal_write_response, \ @@ -113,7 +113,13 @@ def put_internal(resource, payload=None, concurrency_check=False, if payload is None: payload = payload_() - original = get_document(resource, concurrency_check, **lookup) + # Retrieve the original document without checking user-restricted access, + # but returning the document owner in the projection. This allows us to + # prevent PUT if the document exists, but is owned by a different user + # than the currently authenticated one. + original = get_document(resource, concurrency_check, + check_auth_value=False, + force_auth_field_projection=True, **lookup) if not original: if config.UPSERT_ON_PUT: id = lookup[resource_def['id_field']] @@ -126,6 +132,12 @@ def put_internal(resource, payload=None, concurrency_check=False, else: abort(404) + # If the document exists, but is owned by someone else, return + # 403 Forbidden + auth_field, request_auth_value = auth_field_and_value(resource) + if auth_field and original.get(auth_field) != request_auth_value: + abort(403) + last_modified = None etag = None issues = {} diff --git a/eve/tests/auth.py b/eve/tests/auth.py index ffbc02b6f..df179704c 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -643,6 +643,7 @@ def test_put(self): headers=headers, content_type='application/json')) self.assert200(status) + etag = '"%s"' % response['_etag'] # document still accessible with same auth data, status = self.parse_response( @@ -650,6 +651,25 @@ def test_put(self): self.assert200(status) self.assertEqual(data['ref'], new_ref) + # put on same item with different auth fails + original_auth_val = self.resource['authentication'].request_auth_value + self.resource['authentication'].request_auth_value = 'alt' + alt_auth = ('Authorization', 'Basic YWx0OnNlY3JldA==') + alt_changes = {"ref": "1111111111111111111111111"} + headers = [('If-Match', etag), alt_auth] + response, status = self.parse_response( + self.test_client.put(url, data=json.dumps(alt_changes), + headers=headers, + content_type='application/json')) + self.assert403(status) + + # document still accessible with original auth + self.resource['authentication'].request_auth_value = original_auth_val + data, status = self.parse_response( + self.test_client.get(url, headers=self.valid_auth)) + self.assert200(status) + self.assertEqual(data['ref'], new_ref) + def test_put_resource_auth(self): # no global auth. self.app = Eve(settings=self.settings_file) @@ -680,6 +700,7 @@ def test_put_resource_auth(self): headers=headers, content_type='application/json')) self.assert200(status) + etag = '"%s"' % response['_etag'] # document still accessible with same auth data, status = self.parse_response( @@ -687,6 +708,25 @@ def test_put_resource_auth(self): self.assert200(status) self.assertEqual(data['ref'], new_ref) + # put on same item with different auth fails + original_auth_val = resource_def['authentication'].request_auth_value + resource_def['authentication'].request_auth_value = 'alt' + alt_auth = ('Authorization', 'Basic YWx0OnNlY3JldA==') + alt_changes = {"ref": "1111111111111111111111111"} + headers = [('If-Match', etag), alt_auth] + response, status = self.parse_response( + self.app.test_client().put(url, data=json.dumps(alt_changes), + headers=headers, + content_type='application/json')) + self.assert403(status) + + # document still accessible with original auth + resource_def['authentication'].request_auth_value = original_auth_val + data, status = self.parse_response( + self.app.test_client().get(url, headers=self.valid_auth)) + self.assert200(status) + self.assertEqual(data['ref'], new_ref) + def test_put_bandwidth_saver_off_resource_auth(self): """ Test that when BANDWIDTH_SAVER is turned off the auth_field is not exposed in the response payload From 4ceac4daf6347ed7ad803f97439f0cb5b3ac2917 Mon Sep 17 00:00:00 2001 From: Luca Moretto Date: Tue, 3 Apr 2018 16:10:55 +0200 Subject: [PATCH 287/821] Reduced error description details --- eve/io/base.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/eve/io/base.py b/eve/io/base.py index a9a06731a..3dac0506e 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -474,13 +474,7 @@ def _datasource_ex(self, resource, query=None, client_projection=None, self.app.data.get_value_from_query( query, auth_field) != request_auth_value: desc = 'Incompatible User-Restricted Resource ' \ - 'request. Request was for "%s"="%s" but ' \ - '`auth_field` requires "%s"="%s".' % ( - auth_field, - self.app.data.get_value_from_query( - query, auth_field), - auth_field, - request_auth_value) + 'request.' abort(401, description=desc) else: query = self.app.data.combine_queries( From 4d032566054017e686fbf5d9f7bc0c28d19315ef Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 3 Apr 2018 16:59:17 +0200 Subject: [PATCH 288/821] Changelog for #1130 --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index d8f3a0997..7d2597a85 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: PUT behavior with User Restricted Resource Access. Ensure that, under + every circumstance, users are unable to overwrite items owned by other users + (Luca Moretto). - Fix: OPLOG skipped even if ``OPLOG = True``. Closes #1074 (Hung Le). - Fix: Cannot define default projection and request specific field. Closes #1036 (DHuan). From e332c375af9b73b5636d44805e37c4b48403d566 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 4 Apr 2018 15:57:22 +0200 Subject: [PATCH 289/821] Officially deprecate Python 2.6 - python_requires metadata added to setup.py - changelog officially states that Py26 is deprecated. Closes #1129. --- CHANGES | 2 ++ setup.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 7d2597a85..1d25d78d8 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Python 2.6 is deprecated. This is the last release supporting Python 2.6, and + you should upgrade to Python 3 as soon as possible. Closes #1129. - Fix: PUT behavior with User Restricted Resource Access. Ensure that, under every circumstance, users are unable to overwrite items owned by other users (Luca Moretto). diff --git a/setup.py b/setup.py index 04ff3ff01..7deb3e53b 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ test_suite="eve.tests", install_requires=install_requires, tests_require=['redis', 'testfixtures'], + python_requires='>=2.6', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 0f0d0473b1e21ade51c76d41a690b52bda8c57e7 Mon Sep 17 00:00:00 2001 From: Marsch Huynh Date: Sun, 20 Aug 2017 22:05:50 +0700 Subject: [PATCH 290/821] Support partial request for media resource --- docs/features.rst | 22 +++++++++++ eve/endpoints.py | 94 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 8e8de04b4..8ba11f1ec 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1776,6 +1776,28 @@ configuration. Enable ``AUTO_COLLAPSE_MULTI_KEYS`` and ``AUTO_CREATE_LISTS`` to make this possible. This allows to send multiple values for one key in ``multipart/form-data`` requests and in this way upload a list of files. +.. _partial_request: + +Partial request for media resource +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The partial request provides an ability to download a part of a file. +You can also pause and resume when you are downloading without restarting +your download. To use it, make sure you have ``Range`` in your request header. + + .. code-block:: console + + $ curl http://localhost/media/yourfilename -i -H "Range: bytes=0-10" + HTTP/1.1 206 PARTIAL CONTENT + Date: Sun, 20 Aug 2017 14:26:42 GMT + Content-Type: audio/mp4 + Content-Length: 11 + Connection: keep-alive + Content-Range: bytes 0-10/23671 + Last-Modified: Sat, 19 Aug 2017 03:25:36 GMT + Accept-Ranges: bytes + + ftypmp4% + .. _geojson_feature: GeoJSON diff --git a/eve/endpoints.py b/eve/endpoints.py index fc598bfd3..eb5a750ca 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -11,6 +11,8 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import re + from bson import tz_util from flask import abort, request, current_app as app, Response @@ -175,28 +177,76 @@ def media_endpoint(_id): .. versionadded:: 0.6 """ - file_ = app.media.get(_id) - if file_ is None: - return abort(404) - - if_modified_since = weak_date(request.headers.get('If-Modified-Since')) - if if_modified_since is not None: - if if_modified_since.tzinfo is None: - if_modified_since = if_modified_since.replace( - tzinfo=tz_util.utc) - - if if_modified_since > file_.upload_date: - return Response(status=304) - - headers = { - 'Last-Modified': date_to_rfc1123(file_.upload_date), - 'Content-Length': file_.length, - } - - response = Response(file_, headers=headers, mimetype=file_.content_type, - direct_passthrough=True) - - return response + range_header = request.headers.get('Range', None) + if not range_header: + file_ = app.media.get(_id) + if file_ is None: + return abort(404) + + if_modified_since = weak_date(request.headers.get('If-Modified-Since')) + if if_modified_since is not None: + if if_modified_since.tzinfo is None: + if_modified_since = if_modified_since.replace( + tzinfo=tz_util.utc) + + if if_modified_since > file_.upload_date: + return Response(status=304) + + headers = { + 'Last-Modified': date_to_rfc1123(file_.upload_date), + 'Content-Length': file_.length, + 'Accept-Ranges': 'bytes', + } + + response = Response( + file_, + status=200, + headers=headers, + mimetype=file_.content_type, + direct_passthrough=True + ) + + return response + else: + file_ = app.media.get(_id) + size = file_.length + byte1, byte2 = 0, None + + m = re.search('(\d+)-(\d*)', range_header) + g = m.groups() + + if g[0]: + byte1 = int(g[0]) + if g[1]: + byte2 = int(g[1]) + + length = size - byte1 + if byte2 is not None: + length = byte2 - byte1 + 1 + + data = None + file_.seek(byte1) + data = file_.read(length) + + headers = { + 'Last-Modified': date_to_rfc1123(file_.upload_date), + 'Content-Length': file_.length, + 'Accept-Ranges': 'bytes', + 'Content-Range': 'bytes {0}-{1}/{2}'.format( + byte1, + byte1 + length - 1, + size + ), + } + + response = Response( + data, + 206, + headers=headers, + mimetype=file_.content_type, + direct_passthrough=True) + + return response @requires_auth('resource') From ffdeb14aee31cb800d7324679be327247deb4780 Mon Sep 17 00:00:00 2001 From: Marsch Huynh Date: Sun, 24 Dec 2017 19:51:57 +0700 Subject: [PATCH 291/821] fix: media endpoint --- eve/endpoints.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/eve/endpoints.py b/eve/endpoints.py index eb5a750ca..552611311 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -210,22 +210,20 @@ def media_endpoint(_id): else: file_ = app.media.get(_id) size = file_.length - byte1, byte2 = 0, None - - m = re.search('(\d+)-(\d*)', range_header) - g = m.groups() - - if g[0]: - byte1 = int(g[0]) - if g[1]: - byte2 = int(g[1]) - - length = size - byte1 - if byte2 is not None: - length = byte2 - byte1 + 1 + try: + m = re.search('(\d+)-(\d*)', range_header) + begin, end = m.groups() + begin = int(begin) + end = int(end) + except: + begin, end = 0, None + + length = size - begin + if end is not None: + length = end - begin + 1 data = None - file_.seek(byte1) + file_.seek(begin) data = file_.read(length) headers = { @@ -233,8 +231,8 @@ def media_endpoint(_id): 'Content-Length': file_.length, 'Accept-Ranges': 'bytes', 'Content-Range': 'bytes {0}-{1}/{2}'.format( - byte1, - byte1 + length - 1, + begin, + begin + length - 1, size ), } From 5efb01add5bf146a4f6bd19548d6f31d9875baf3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 5 Apr 2018 10:39:27 +0200 Subject: [PATCH 292/821] A little refactoring (DRY). Addresses #1050. --- eve/endpoints.py | 87 +++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/eve/endpoints.py b/eve/endpoints.py index 552611311..6820f2c20 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -177,38 +177,20 @@ def media_endpoint(_id): .. versionadded:: 0.6 """ - range_header = request.headers.get('Range', None) - if not range_header: - file_ = app.media.get(_id) - if file_ is None: - return abort(404) - - if_modified_since = weak_date(request.headers.get('If-Modified-Since')) - if if_modified_since is not None: - if if_modified_since.tzinfo is None: - if_modified_since = if_modified_since.replace( - tzinfo=tz_util.utc) + file_ = app.media.get(_id) + if file_ is None: + return abort(404) - if if_modified_since > file_.upload_date: - return Response(status=304) + headers = { + 'Last-Modified': date_to_rfc1123(file_.upload_date), + 'Content-Length': file_.length, + 'Accept-Ranges': 'bytes', + } - headers = { - 'Last-Modified': date_to_rfc1123(file_.upload_date), - 'Content-Length': file_.length, - 'Accept-Ranges': 'bytes', - } - - response = Response( - file_, - status=200, - headers=headers, - mimetype=file_.content_type, - direct_passthrough=True - ) + range_header = request.headers.get('Range') + if range_header: + status = 206 - return response - else: - file_ = app.media.get(_id) size = file_.length try: m = re.search('(\d+)-(\d*)', range_header) @@ -222,29 +204,36 @@ def media_endpoint(_id): if end is not None: length = end - begin + 1 - data = None file_.seek(begin) + data = file_.read(length) + headers['Content-Range'] = 'bytes {0}-{1}/{2}'.format( + begin, + begin + length - 1, + size + ) + else: + if_modified_since = weak_date(request.headers.get('If-Modified-Since')) + if if_modified_since: + if not if_modified_since.tzinfo: + if_modified_since = if_modified_since.replace( + tzinfo=tz_util.utc) + + if if_modified_since > file_.upload_date: + return Response(status=304) + + data = file_ + status = 200 + + response = Response( + data, + status=status, + headers=headers, + mimetype=file_.content_type, + direct_passthrough=True + ) - headers = { - 'Last-Modified': date_to_rfc1123(file_.upload_date), - 'Content-Length': file_.length, - 'Accept-Ranges': 'bytes', - 'Content-Range': 'bytes {0}-{1}/{2}'.format( - begin, - begin + length - 1, - size - ), - } - - response = Response( - data, - 206, - headers=headers, - mimetype=file_.content_type, - direct_passthrough=True) - - return response + return response @requires_auth('resource') From bf8898227dcbf3ecdf1444035da2c0620c39abb8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 5 Apr 2018 10:40:36 +0200 Subject: [PATCH 293/821] Test coverage for #1050 --- eve/tests/io/media.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 29c441fed..7c21736ce 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -378,6 +378,28 @@ def test_gridfs_media_storage_return_url(self): response = self.test_client.get(url) self.assertEqual(self.clean, response.get_data()) + def test_gridfs_partial_media(self): + self.app._init_media_endpoint() + self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False + self.app.config['RETURN_MEDIA_AS_URL'] = True + + r, s = self._post() + _id = r[self.id_field] + where = 'where={"%s": "%s"}' % (self.id_field, _id) + r, s = self.parse_response( + self.test_client.get('%s?%s' % (self.url, where))) + url = r['_items'][0]['media'] + + headers = {'Range': 'bytes=0-5'} + response = self.test_client.get(url, headers=headers) + self.assertEqual(self.clean[:6], response.get_data()) + headers = {'Range': 'bytes=5-10'} + response = self.test_client.get(url, headers=headers) + self.assertEqual(self.clean[5:11], response.get_data()) + headers = {'Range': 'bytes=0-999'} + response = self.test_client.get(url, headers=headers) + self.assertEqual(self.clean, response.get_data()) + def test_gridfs_media_storage_base_url(self): self.app._init_media_endpoint() self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False From df967dd85af515e8165f370bf2877a13c95f32ae Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 5 Apr 2018 10:43:22 +0200 Subject: [PATCH 294/821] Changelog for #1050 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 1d25d78d8..f7e0d75be 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: support for partial media requests. Clients can request partial file + downloads by adding a ``Range`` header to their media request (Marsch Huynh). - Python 2.6 is deprecated. This is the last release supporting Python 2.6, and you should upgrade to Python 3 as soon as possible. Closes #1129. - Fix: PUT behavior with User Restricted Resource Access. Ensure that, under From 0a72e3427ce5f928947b7dca6712fb0c397b901b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 5 Apr 2018 10:54:56 +0200 Subject: [PATCH 295/821] Improve partial downloads documentation --- docs/features.rst | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 8ba11f1ec..5e01d2627 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1690,6 +1690,31 @@ set your media endpoint like so: Setting ``MEDIA_BASE_URL`` is optional. If no value is set, then the API base address will be used when building the URL for ``MEDIA_ENDPOINT``. +.. _partial_request: + +Partial media downloads +~~~~~~~~~~~~~~~~~~~~~~~ +When files are served at a dedicated endpoint, clients can request partial +downloads. This allows them to provide features such as optimized +pause/resume (with no need to restart the download). To perform a partial +download, make sure the ``Range`` header is added the the client request. + + .. code-block:: console + + $ curl http://localhost/media/yourfile -i -H "Range: bytes=0-10" + HTTP/1.1 206 PARTIAL CONTENT + Date: Sun, 20 Aug 2017 14:26:42 GMT + Content-Type: audio/mp4 + Content-Length: 11 + Connection: keep-alive + Content-Range: bytes 0-10/23671 + Last-Modified: Sat, 19 Aug 2017 03:25:36 GMT + Accept-Ranges: bytes + + abcdefghilm + +In the snippet above, we see curl requesting the first chunk of a file. + .. _projection_filestorage: Leveraging Projections to optimize the handling of media files @@ -1776,28 +1801,6 @@ configuration. Enable ``AUTO_COLLAPSE_MULTI_KEYS`` and ``AUTO_CREATE_LISTS`` to make this possible. This allows to send multiple values for one key in ``multipart/form-data`` requests and in this way upload a list of files. -.. _partial_request: - -Partial request for media resource -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The partial request provides an ability to download a part of a file. -You can also pause and resume when you are downloading without restarting -your download. To use it, make sure you have ``Range`` in your request header. - - .. code-block:: console - - $ curl http://localhost/media/yourfilename -i -H "Range: bytes=0-10" - HTTP/1.1 206 PARTIAL CONTENT - Date: Sun, 20 Aug 2017 14:26:42 GMT - Content-Type: audio/mp4 - Content-Length: 11 - Connection: keep-alive - Content-Range: bytes 0-10/23671 - Last-Modified: Sat, 19 Aug 2017 03:25:36 GMT - Accept-Ranges: bytes - - ftypmp4% - .. _geojson_feature: GeoJSON From e5d6f4758dcc68824e150aa50bb7bbecb6970136 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 5 Apr 2018 10:56:31 +0200 Subject: [PATCH 296/821] Marsch Huynh --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 26c4c3917..971276f39 100644 --- a/AUTHORS +++ b/AUTHORS @@ -103,6 +103,7 @@ Patches and Contributions - Marcus Cobden - Marica Odagaki - Mario Kralj +- Marsch Huynh - Martin Fous - Massimo Scamarcia - Mateusz Łoskot From 6ad36fb08018dfaa198b5772793600707fa7e4c9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 6 Apr 2018 10:57:21 +0200 Subject: [PATCH 297/821] Add support for mongo $box geo query operator Closes #1122. --- CHANGES | 4 +++- eve/io/mongo/mongo.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index f7e0d75be..42674c6a0 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: Add suport for mongo's ``$box`` geo query operator. Closes #1122. - New: support for partial media requests. Clients can request partial file downloads by adding a ``Range`` header to their media request (Marsch Huynh). - Python 2.6 is deprecated. This is the last release supporting Python 2.6, and @@ -46,7 +47,8 @@ Version 0.8 - New: when the media endpoint is enabled, the default authentication class will be used to secure it. Closes #1083. - New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, - ``bitsAllSet``, ``bitsAnyClear``, ``bitsAnySet``. Closes 1053 (Qiang Zhang). + ``$bitsAllSet``, ``$bitsAnyClear``, ``$bitsAnySet``. Closes 1053 (Qiang + Zhang). - Fix: Aggregation query parameter does not replace keys in the lists. Closes #1025 (Serge Kir). - Fix: Removed OrderedDict dependency; use ``OrderedDict`` from diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 828e767ee..b81d83808 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -105,7 +105,7 @@ class Mongo(DataLayer): ['$options', '$search', '$language', '$caseSensitive'] + ['$diacriticSensitive', '$exists', '$type'] + ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + - ['$geometry', '$maxDistance'] + + ['$geometry', '$maxDistance', '$box'] + ['$all', '$elemMatch', '$size'] + ['$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'] ) From 1a5643f41e4f80ed030314365b60d649e21ac451 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 13 Apr 2018 16:57:38 +0200 Subject: [PATCH 298/821] Get rid of unwanted .vscode folder --- .gitignore | 1 + .vscode/launch.json | 186 ------------------------------------------ .vscode/settings.json | 8 -- 3 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 8754ce46b..76838eaee 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ _build .idea .cache +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 65608537d..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "program": "${file}", - "cwd": "${workspaceFolder}", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - }, - { - "name": "Python: Attach", - "type": "python", - "request": "attach", - "localRoot": "${workspaceFolder}", - "remoteRoot": "${workspaceFolder}", - "port": 3000, - "secret": "my_secret", - "host": "localhost" - }, - { - "name": "Python: Terminal (integrated)", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "program": "${file}", - "cwd": "", - "console": "integratedTerminal", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [] - }, - { - "name": "Python: Terminal (external)", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "program": "${file}", - "cwd": "", - "console": "externalTerminal", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [] - }, - { - "name": "Python: Django", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "program": "${workspaceFolder}/manage.py", - "cwd": "${workspaceFolder}", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput", - "DjangoDebugging" - ] - }, - { - "name": "Python: Flask (0.11.x or later)", - "type": "python", - "request": "launch", - "stopOnEntry": false, - "pythonPath": "${config:python.pythonPath}", - "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", - "cwd": "${workspaceFolder}", - "env": { - "FLASK_APP": "${workspaceFolder}/quickstart/app.py" - }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - }, - { - "name": "Python: Flask (0.10.x or earlier)", - "type": "python", - "request": "launch", - "stopOnEntry": false, - "pythonPath": "${config:python.pythonPath}", - "program": "${workspaceFolder}/run.py", - "cwd": "${workspaceFolder}", - "args": [], - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - }, - { - "name": "Python: PySpark", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "osx": { - "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" - }, - "windows": { - "pythonPath": "${env:SPARK_HOME}/bin/spark-submit.cmd" - }, - "linux": { - "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" - }, - "program": "${file}", - "cwd": "${workspaceFolder}", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - }, - { - "name": "Python: Module", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "module": "module.name", - "cwd": "${workspaceFolder}", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - }, - { - "name": "Python: Pyramid", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "cwd": "${workspaceFolder}", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "args": [ - "${workspaceFolder}/development.ini" - ], - "debugOptions": [ - "RedirectOutput", - "Pyramid" - ] - }, - { - "name": "Python: Watson", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", - "program": "${workspaceFolder}/console.py", - "cwd": "${workspaceFolder}", - "args": [ - "dev", - "runserver", - "--noreload=True" - ], - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a9c43166f..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "python.pythonPath": "/Users/nicola/.virtualenvs/eve/bin/python3", - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--ignore=E731,E722,F821", - ], - "python.linting.pylintEnabled": false, -} \ No newline at end of file From bfb6fb18457c2b1568aa608692d7e8b4456c38e6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 20 Apr 2018 10:37:19 +0200 Subject: [PATCH 299/821] Add the TalkPython Eve course to the documentation --- CHANGES | 2 ++ docs/_templates/sidebarintro.html | 28 ++++++++++++++++++---------- docs/funding.rst | 8 ++++++++ docs/tutorials/index.rst | 8 ++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 42674c6a0..09ac3e4ae 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Docs: Add link to the Eve course. It was authored by the project author, and + it is hosted by TalkPython Training. - New: Add suport for mongo's ``$box`` geo query operator. Closes #1122. - New: support for partial media requests. Clients can request partial file downloads by adding a ``Range`` header to their media request (Marsch Huynh). diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 10c1a9062..8b92ce18e 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -8,6 +8,24 @@

    Stay Informed

    Join Mailing List.

    +

    Eve Course

    +

    This course will teach you how to effortlessly build RESTful services based on Flask, Eve, and MongoDB.

    +

    The teacher is the project creator and maintainer.

    + + +

    Useful Links

    + +

    Other Projects

    More Nicola Iarocci projects:

    @@ -22,14 +40,4 @@

    Other Projects

  • Eve-OAuth2
  • -

    Useful Links

    -

    You are looking at the documentation of the development version.

    diff --git a/docs/funding.rst b/docs/funding.rst index 790666f4b..3f0ed5e78 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -31,6 +31,13 @@ You can support Eve development by pledging on Patreon or donating on PayPal. - `Become a Backer `_ (recurring pledge) - `Donate via PayPal `_ (one time) +Eve Course at TalkPython Training +--------------------------------- +There is a 5 hours-long Eve course available for you at the fine TalkPython +Training website. The teacher is Nicola, Eve author and maintainer. Taking this +course will directly support the project. + +- `Take the Eve Course at TalkPython Training `_ Custom Sponsorship and Consulting --------------------------------- @@ -39,3 +46,4 @@ open to conversations regarding custom sponsorship / consulting arrangements. Just `get in touch`_ with me. .. _`get in touch`: mailto:nicola@nicolaiarocci.com +.. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 75010b027..5d14a0000 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -8,3 +8,11 @@ Tutorials account_management custom_idfields + +Learn Eve at TalkPython Training +-------------------------------- +There is a 5 hours-long Eve course available for you at the fine TalkPython +Training website. The teacher is Nicola, Eve author and maintainer. Taking this +course will directly support the project. + +- `Take the Eve Course at TalkPython Training `_ From d2f41b8ac066a659e98588a21be17259f8d07fc4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 20 Apr 2018 10:47:10 +0200 Subject: [PATCH 300/821] Reduce noise --- docs/_templates/sidebarintro.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 8b92ce18e..9b6009cb4 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -9,7 +9,7 @@

    Stay Informed

    Join Mailing List.

    Eve Course

    -

    This course will teach you how to effortlessly build RESTful services based on Flask, Eve, and MongoDB.

    +

    This course will teach you how to build RESTful services with Eve and MongoDB.

    The teacher is the project creator and maintainer.

    • Eve Course @ TalkPython
    • From a0e1417c4b93738bc6f5617f977973ec30575a1d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 23 Apr 2018 10:07:14 +0200 Subject: [PATCH 301/821] Fix validation crash under Cerberus 1.2 Now cerberus raises an exception when a schema validation rule is non-existant, which surfaced a previous typo. Closes #1137. --- CHANGES | 1 + eve/validation.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 09ac3e4ae..95b243a51 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Fix: Crash with Cerberus 1.2. Closes #1137. - Docs: Add link to the Eve course. It was authored by the project author, and it is hosted by TalkPython Training. - New: Add suport for mongo's ``$box`` geo query operator. Closes #1122. diff --git a/eve/validation.py b/eve/validation.py index 577a81ced..0d5c5016e 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -79,7 +79,7 @@ def _normalize_default_setter(self, mapping, schema, field): field) def _validate_dependencies(self, dependencies, field, value): - """ {'type': ['dict', 'hashable', 'hashables']} """ + """ {'type': ['dict', 'hashable', 'list']} """ persisted = self._filter_persisted_fields_not_in_document(dependencies) if persisted: dcopy = copy.copy(self.document) From 8c3588f6eb15f1f11ac9714203b902435b5c6571 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 24 Apr 2018 16:22:38 +0200 Subject: [PATCH 302/821] Add support for before_/after_aggregation events. Closes #1057. --- CHANGES | 2 ++ docs/features.rst | 35 ++++++++++++++++++ eve/methods/get.py | 4 +++ eve/tests/methods/get.py | 78 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) diff --git a/CHANGES b/CHANGES index 95b243a51..9dafab639 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: ``before_aggregation`` and ``after_aggregation`` event hooks allow to + attach custom callbacks to aggregation endpoints. Closes #1057. - Fix: Crash with Cerberus 1.2. Closes #1137. - Docs: Add link to the Eve course. It was authored by the project author, and it is hosted by TalkPython Training. diff --git a/docs/features.rst b/docs/features.rst index 5e01d2627..0e61c6543 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1534,6 +1534,38 @@ notified of such a disastrous occurrence by hooking a callback function to the hit by the DELETE after having retrieved the original document. NOTE: those two event are useful in order to perform some business logic before the actual remove operation given the look up and the list of originals +.. _aggregation_hooks: + +Aggregation event hooks +~~~~~~~~~~~~~~~~~~~~~~~ +You can also attach one or more callbacks to your aggregation endpoints. The +``before_aggregation`` event is fired when an aggregation is about to be +performed. Any attached callback function will receive both the endpoint name +and the aggregation pipeline as arguments. The pipeline can then be altered if +needed. + +.. code-block:: pycon + + >>> def on_aggregate(endpoint, pipeline): + ... pipeline.append({"$unwind": "$tags"}) + + >>> app = Eve() + >>> app.before_aggregation += on_aggregate + +The ``after_aggregation`` event is fired when the aggregation has been +performed. An attached callback function could leverage this event to modify +the documents before they are returned to the client. + +.. code-block:: pycon + + >>> def alter_documents(endpoint, documents): + ... for document in documents: + ... document['hello'] = 'well, hello!' + + >>> app = Eve() + >>> app.after_aggregation += alter_documents + +For more information on aggregation support, see :ref:`aggregation` .. admonition:: Please note @@ -2204,6 +2236,8 @@ to a keyword of your liking, just set ``QUERY_AGGREGATION`` in your settings. You can also set all options natively supported by PyMongo. For more informations on aggregation see :ref:`datasource`. +Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. + Limitations ~~~~~~~~~~~ ``HATEOAS`` is not available at aggregation endpoints. This should not @@ -2237,6 +2271,7 @@ A single endpoint cannot serve both regular and aggregation results. However, since it is possible to setup multiple endpoints all serving from the same datasource (see :ref:`source`), similar functionality can be easily achieved. + MongoDB and SQL Support ------------------------ Support for single or multiple MongoDB database/servers comes out of the box. diff --git a/eve/methods/get.py b/eve/methods/get.py index 213eb31ad..d68c58565 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -166,11 +166,15 @@ def parse_again(st_value, key, value): req_pipeline.append(skip) req_pipeline.append(limit) + getattr(app, "before_aggregation")(resource, req_pipeline) + cursor = app.data.aggregate(resource, req_pipeline, options) for document in cursor: documents.append(document) + getattr(app, "after_aggregation")(resource, documents) + response[config.ITEMS] = documents # PyMongo's CommandCursor does not return a count, so we cannot diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 149a871c7..241fb668d 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1316,6 +1316,9 @@ def test_get_aggregation_endpoint(self): ] ) + self.devent = DummyEvent(lambda: True) + self.app.before_aggregation += self.devent + self.app.register_resource( 'aggregate_test', { 'datasource': { @@ -1333,6 +1336,7 @@ def test_get_aggregation_endpoint(self): response, status = self.get('aggregate_test?aggregate=ciao') self.assert400(status) + self.assertTrue(self.devent.called is None) def assertOutput(doc, count, id): self.assertEqual(doc['count'], count) @@ -1345,6 +1349,7 @@ def assertOutput(doc, count, id): assertOutput(docs[0], 3, 'cat') assertOutput(docs[1], 2, 'dog') assertOutput(docs[2], 1, 'mouse') + self.assertEqual('aggregate_test', self.devent.called[0]) response, status = self.get('aggregate_test?aggregate={"$field1":2}') self.assert200(status) @@ -1353,6 +1358,7 @@ def assertOutput(doc, count, id): assertOutput(docs[0], 6, 'cat') assertOutput(docs[1], 4, 'dog') assertOutput(docs[2], 2, 'mouse') + self.assertEqual('aggregate_test', self.devent.called[0]) # this will return 0 for all documents 'count' fields as no $field1 # will be gien with the query (actually, no query will be there at all) @@ -1363,10 +1369,12 @@ def assertOutput(doc, count, id): self.assertEqual(docs[0]['count'], 0) self.assertEqual(docs[1]['count'], 0) self.assertEqual(docs[2]['count'], 0) + self.assertEqual('aggregate_test', self.devent.called[0]) # malformed field name is ignored response, status = self.get('aggregate_test?aggregate={"field1":1}') self.assert200(status) + self.assertEqual('aggregate_test', self.devent.called[0]) # unknown field is ignored response, status = self.get('aggregate_test?aggregate={"$unknown":1}') @@ -2045,6 +2053,76 @@ def test_on_fetched_item_contacts(self): self.assertEqual(self.item_id, str(self.devent.called[0][id_field])) self.assertEqual(1, len(self.devent.called)) + def test_get_before_aggregation_hook(self): + _db = self.connection[MONGO_DBNAME] + _db.aggregate_test.insert_many( + [ + {"x": 1, "tags": ["dog", "cat"]}, + {"x": 2, "tags": ["cat"]}, + {"x": 2, "tags": ["mouse", "cat", "dog"]}, + {"x": 3, "tags": []} + ] + ) + + self.app.before_aggregation += self.devent + + self.app.register_resource( + 'aggregate_test', { + 'datasource': { + 'aggregation': { + 'pipeline': [ + {"$unwind": "$tags"}, + {"$group": {"_id": "$tags", "count": {"$sum": + "$field1"}}}, + ], + } + } + } + ) + + response, status = self.get('aggregate_test?aggregate=ciao') + self.assert400(status) + self.assertTrue(self.devent.called is None) + + response, status = self.get('aggregate_test?aggregate={"$field1":1}') + self.assert200(status) + self.assertEqual('aggregate_test', self.devent.called[0]) + + def test_get_after_aggregation_hook(self): + _db = self.connection[MONGO_DBNAME] + _db.aggregate_test.insert_many( + [ + {"x": 1, "tags": ["dog", "cat"]}, + {"x": 2, "tags": ["cat"]}, + {"x": 2, "tags": ["mouse", "cat", "dog"]}, + {"x": 3, "tags": []} + ] + ) + + self.app.after_aggregation += self.devent + + self.app.register_resource( + 'aggregate_test', { + 'datasource': { + 'aggregation': { + 'pipeline': [ + {"$unwind": "$tags"}, + {"$group": {"_id": "$tags", "count": {"$sum": + "$field1"}}}, + ], + } + } + } + ) + + response, status = self.get('aggregate_test?aggregate=ciao') + self.assert400(status) + self.assertTrue(self.devent.called is None) + + response, status = self.get('aggregate_test?aggregate={"$field1":1}') + self.assert200(status) + self.assertEqual('aggregate_test', self.devent.called[0]) + def get_resource(self): return self.test_client.get(self.known_resource_url) From b92581f0b423070020f0948ba57f7db9aeacac00 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 27 Apr 2018 14:49:09 +0200 Subject: [PATCH 303/821] Bump flask requirement to 0.1 --- CHANGES | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 9dafab639..3f0d41c01 100644 --- a/CHANGES +++ b/CHANGES @@ -32,7 +32,7 @@ Version 0.8 sub-document fields. Closes #1123 (Luca Moretto). - Fix documentation typos (Olof Johansson) - Fix a changelog typo (kreynen). -- Update: bump Flask requirement to <=0.13. Closes #1111. +- Update: bump Flask requirement to <=1.0. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You diff --git a/requirements.txt b/requirements.txt index e536aae3f..d2613b7f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Cerberus==1.1 Events==0.3 -Flask==0.12.2 +Flask==0.12.3 itsdangerous==0.24 Jinja2==2.10 MarkupSafe==0.23 diff --git a/setup.py b/setup.py index 7deb3e53b..6ddbb5ff4 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ 'markupsafe>=0.23,<1.0', 'jinja2>=2.8,<3.0', 'itsdangerous>=0.24,<1.0', - 'flask>=0.10.1,<=0.13', + 'flask>=0.10.1,<1.0', 'pymongo>=3.5', ] From 65b945e99f753d497a243ee5a1e3f6d2bcd55e76 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 27 Apr 2018 15:04:47 +0200 Subject: [PATCH 304/821] Add PYPI-friendly metadata to setup.py --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 6ddbb5ff4..d7e50da8b 100755 --- a/setup.py +++ b/setup.py @@ -35,6 +35,11 @@ author='Nicola Iarocci', author_email='eve@nicolaiarocci.com', url='http://python-eve.org', + project_urls={ + 'Documentation': 'http://python-eve.org', + 'Code': 'https://github.com/pyeve/eve', + 'Issue tracker': 'https://github.com/pyeve/eve/issues', + }, license='BSD', platforms=["any"], packages=find_packages(), From fdf87bbb03a91bcb1dde368e25eeb0aba60323c6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 3 May 2018 15:48:27 +0200 Subject: [PATCH 305/821] Tests: get rid of most deprecation warnings All pymongo and most Python 3 deprecation warnings have been taken care of. --- CHANGES | 2 ++ eve/io/mongo/mongo.py | 2 +- eve/tests/__init__.py | 20 +++++++----- eve/tests/auth.py | 4 +-- eve/tests/endpoints.py | 5 ++- eve/tests/io/flask_pymongo.py | 15 ++++++--- eve/tests/io/mongo.py | 7 ++--- eve/tests/io/multi_mongo.py | 12 +++++-- eve/tests/methods/delete.py | 34 ++++++++++---------- eve/tests/methods/get.py | 59 ++++++++++++++++++----------------- eve/tests/methods/patch.py | 10 +++--- eve/tests/methods/put.py | 25 +++++++-------- eve/tests/renders.py | 6 ++-- eve/tests/test_settings.py | 2 +- 14 files changed, 109 insertions(+), 94 deletions(-) diff --git a/CHANGES b/CHANGES index 3f0d41c01..4f5333e51 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Development Version 0.8 ~~~~~~~~~~~ +- Tests: finally acknowledge the existence of modern APIs for both Mongo and + Python (get rid of most deprecation warnings). - New: ``before_aggregation`` and ``after_aggregation`` event hooks allow to attach custom callbacks to aggregation endpoints. Closes #1057. - Fix: Crash with Cerberus 1.2. Closes #1137. diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b81d83808..da2a14928 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -502,7 +502,7 @@ def _change_request(self, resource, id_, changes, original, replace=False): ): # attempt to update an immutable field. this usually # happens when a PATCH or PUT includes a mismatching ID_FIELD. - self.app.logger.warn(e) + self.app.logger.warning(e) description = debug_error_message( 'pymongo.errors.OperationFailure: %s' % e) or \ "Attempt to update an immutable field. Usually happens " \ diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index e3bdd6d3c..c78c242fc 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -332,8 +332,10 @@ def setupDB(self): self.connection = MongoClient(MONGO_HOST, MONGO_PORT) self.connection.drop_database(MONGO_DBNAME) if MONGO_USERNAME: - self.connection[MONGO_DBNAME].add_user(MONGO_USERNAME, - MONGO_PASSWORD) + db = self.connection[MONGO_DBNAME] + db.command('dropUser', MONGO_USERNAME) + db.command('createUser', MONGO_USERNAME, pwd=MONGO_PASSWORD, + roles=['dbAdmin']) self.bulk_insert() def bulk_insert(self): @@ -564,11 +566,13 @@ def generate_products(self): def bulk_insert(self): _db = self.connection[MONGO_DBNAME] - _db.contacts.insert(self.random_contacts(self.known_resource_count)) - _db.contacts.insert(self.random_users(2)) - _db.payments.insert(self.random_payments(10)) - _db.invoices.insert(self.random_invoices(1)) - _db.internal_transactions.insert(self.random_internal_transactions(4)) + _db.contacts.insert_many(self.random_contacts( + self.known_resource_count)) + _db.contacts.insert_many(self.random_users(2)) + _db.payments.insert_many(self.random_payments(10)) + _db.invoices.insert_many(self.random_invoices(1)) + _db.internal_transactions.insert_many( + self.random_internal_transactions(4)) products = self.generate_products() - _db.products.insert(products) + _db.products.insert_many(products) self.connection.close() diff --git a/eve/tests/auth.py b/eve/tests/auth.py index df179704c..f2d2ddf41 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -450,7 +450,7 @@ def test_get(self): new_user = self.random_contacts(1)[0] new_user['username'] = 'admin' _db = self.connection[self.app.config['MONGO_DBNAME']] - _db.contacts.insert(new_user) + _db.contacts.insert_one(new_user) # Verify that we can retrieve it data2, status2 = self.parse_response( @@ -511,7 +511,7 @@ def test_filter_by_auth_field_id(self): new_user['_id'] = _id new_user['username'] = 'admin' _db = self.connection[self.app.config['MONGO_DBNAME']] - _db.contacts.insert(new_user) + _db.contacts.insert_one(new_user) # Retrieving /the same/ user by id returns OK filter_query_2 = filter_by_id % 'deadbeefdeadbeefdeadbeef' diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index d6152fee3..67751a357 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -91,8 +91,7 @@ def bulk_insert(self): # create a document which has a id field of UUID type and store it # into the database _db = self.connection[MONGO_DBNAME] - fake = {'_id': UUID(self.uuid_valid), } - _db.uuids.insert(fake) + _db.uuids.insert_one({'_id': UUID(self.uuid_valid)}) def _get_etag(self): r = self.test_client.get(self.url) @@ -294,7 +293,7 @@ def on_generic_inserted(self, resource, docs): config.LAST_UPDATED: dt, config.DATE_CREATED: dt, } - self.app.data.insert('internal_transactions', [transaction]) + self.app.data.insert('internal_transactions', transaction) def test_internal_endpoint(self): self.app.on_inserted -= self.on_generic_inserted diff --git a/eve/tests/io/flask_pymongo.py b/eve/tests/io/flask_pymongo.py index 2849aac99..93ef39182 100644 --- a/eve/tests/io/flask_pymongo.py +++ b/eve/tests/io/flask_pymongo.py @@ -26,14 +26,14 @@ def test_auth_params_provided_in_mongo_url(self): MONGO_HOST, MONGO_PORT) with self.app.app_context(): db = PyMongo(self.app, 'MONGO1').db - self.assertEquals(0, db.works.count()) + self.assertEqual(0, db.works.count()) def test_auth_params_provided_in_config(self): self.app.config['MONGO1_USERNAME'] = MONGO1_USERNAME self.app.config['MONGO1_PASSWORD'] = MONGO1_PASSWORD with self.app.app_context(): db = PyMongo(self.app, 'MONGO1').db - self.assertEquals(0, db.works.count()) + self.assertEqual(0, db.works.count()) def test_invalid_auth_params_provided(self): # if bad username and/or password is provided in MONGO_URL and mongo @@ -56,13 +56,18 @@ def test_valid_port(self): self.app.config['MONGO1_PORT'] = 27017 with self.app.app_context(): db = PyMongo(self.app, 'MONGO1').db - self.assertEquals(0, db.works.count()) + self.assertEqual(0, db.works.count()) def _setupdb(self): self.connection = MongoClient() self.connection.drop_database(MONGO1_DBNAME) - self.connection[MONGO1_DBNAME].add_user(MONGO1_USERNAME, - MONGO1_PASSWORD) + db = self.connection[MONGO1_DBNAME] + try: + db.command('dropUser', MONGO1_USERNAME) + except OperationFailure: + pass + db.command('createUser', MONGO1_USERNAME, pwd=MONGO1_PASSWORD, + roles=['dbAdmin']) def _pymongo_instance(self): with self.app.app_context(): diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index 83a91448b..30bfba6b5 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -421,8 +421,7 @@ def test_query_contains_field(self): def test_delete_returns_status(self): db = self.connection[MONGO_DBNAME] count = db.contacts.count() - result = db.contacts.remove() - self.assertTrue(isinstance(result, dict)) - self.assertEqual(result.get('n'), count) - self.assertEqual(result.get('ok'), 1) + result = db.contacts.delete_many({}) + self.assertEqual(count, result.deleted_count) + self.assertEqual(True, result.acknowledged) self.connection.close() diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index 29af22366..318dd5c11 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -4,6 +4,7 @@ import json from bson import ObjectId from pymongo import MongoClient +from pymongo.errors import OperationFailure import eve from eve.auth import BasicAuth @@ -37,8 +38,13 @@ def tearDown(self): def setupDB2(self): self.connection = MongoClient() self.connection.drop_database(MONGO1_DBNAME) - self.connection[MONGO1_DBNAME].add_user(MONGO1_USERNAME, - MONGO1_PASSWORD) + db = self.connection[MONGO1_DBNAME] + try: + db.command('dropUser', MONGO1_USERNAME) + except OperationFailure: + pass + db.command('createUser', MONGO1_USERNAME, pwd=MONGO1_PASSWORD, + roles=['dbAdmin']) self.bulk_insert2() def dropDB2(self): @@ -49,7 +55,7 @@ def dropDB2(self): def bulk_insert2(self): _db = self.connection[MONGO1_DBNAME] works = self.random_works(self.known_resource_count) - _db.works.insert(works) + _db.works.insert_many(works) self.work = _db.works.find_one() self.connection.close() diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index e14959410..d7179ab2f 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -169,20 +169,20 @@ def test_delete_subresource(self): _db = self.connection[MONGO_DBNAME] # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # grab parent collection count; we will use this later to make sure we # didn't delete all the users in the datanase. We add one extra invoice # to make sure that the actual count will never be 1 (which would # invalidate the test) - _db.invoices.insert({'inv_number': 1}) + _db.invoices.insert_one({'inv_number': 1}) response, status = self.get('invoices') invoices = len(response[self.app.config['ITEMS']]) # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # verify that the only document retrieved is referencing the correct # parent document @@ -207,12 +207,12 @@ def test_delete_subresource_item(self): _db = self.connection[MONGO_DBNAME] # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices/%s' % @@ -426,11 +426,11 @@ def test_softdeleted_embedded_doc(self): """ # Set up and confirm embedded document _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id fake_contact_url = self.known_resource_url + "/" + str(fake_contact_id) - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) invoices = self.domain['invoices'] invoices['embedding'] = True @@ -467,10 +467,10 @@ def test_softdeleted_get_response_skips_embedded_expansion(self): """ # Confirm embedded document works before delete _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) invoices = self.domain['invoices'] invoices['embedding'] = True diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 241fb668d..bf2f4d10b 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -617,7 +617,7 @@ def test_documents_missing_standard_date_fields(self): ref = 'test_update_field' contacts[0]['ref'] = ref _db = self.connection[MONGO_DBNAME] - _db.contacts.insert(contacts) + _db.contacts.insert_one(contacts[0]) where = '{"ref": "%s"}' % ref response, status = self.get(self.known_resource, '?where=%s' % where) @@ -842,10 +842,10 @@ def test_get_embedded(self): # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) invoices = self.domain['invoices'] @@ -941,10 +941,10 @@ def test_get_custom_embedded(self): # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) invoices = self.domain['invoices'] invoices['schema']['person']['data_relation']['embeddable'] = True @@ -963,19 +963,20 @@ def test_get_reference_embedded_in_subdocuments(self): _db = self.connection[MONGO_DBNAME] holding_contacts = self.random_contacts(2) - holding_contact_ids = _db.contacts.insert(holding_contacts) + holding_contact_ids = \ + _db.contacts.insert_many(holding_contacts).inserted_ids contacts = self.random_contacts(2) - contact_ids = _db.contacts.insert(contacts) + contact_ids = _db.contacts.insert_many(contacts).inserted_ids holding = {'departments': [{'title': 'managment', 'members': holding_contact_ids}]} - holding_id = _db.companies.insert(holding) + holding_id = _db.companies.insert_one(holding).inserted_id company = {'holding': holding_id, 'departments': [{'title': 'development', 'members': contact_ids}]} - company_id = _db.companies.insert(company) + company_id = _db.companies.insert_one(company).inserted_id # Add a documents with no reference that should be ignored - _db.companies.insert({}) - _db.companies.insert({'departments': []}) + _db.companies.insert_one({}) + _db.companies.insert_one({'departments': []}) companies = self.domain['companies'] contact_ids = list(map(str, contact_ids)) @@ -1093,11 +1094,11 @@ def test_get_subresource(self): _db = self.connection[MONGO_DBNAME] # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices' % fake_contact_id) @@ -1295,7 +1296,7 @@ def test_get_subresource_with_custom_idfield(self): 'title': 'Child product', 'parent_product': parent_product_sku } - db.products.insert(product) + db.products.insert_one(product) response, status = self.get('products/%s/children' % parent_product_sku) self.assert200(status) @@ -1708,7 +1709,7 @@ def test_getitem_missing_standard_date_fields(self): ref = 'test_update_field' contacts[0]['ref'] = ref _db = self.connection[MONGO_DBNAME] - _db.contacts.insert(contacts) + _db.contacts.insert_one(contacts[0]) response, status = self.get(self.known_resource, item=ref) self.assertItemResponse(response, status) @@ -1723,10 +1724,10 @@ def test_getitem_embedded(self): # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) invoices = self.domain['invoices'] @@ -1819,11 +1820,11 @@ def test_subresource_getitem(self): _db = self.connection[MONGO_DBNAME] # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices/%s' % (fake_contact_id, diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 1963470b5..8e5c653fe 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -380,7 +380,7 @@ def test_patch_missing_standard_date_fields(self): ref = 'test_update_field' contacts[0]['ref'] = ref _db = self.connection[MONGO_DBNAME] - _db.contacts.insert(contacts) + _db.contacts.insert_one(contacts[0]) # now retrieve same document via API and get its etag, which is # supposed to be computed on default DATE_CREATED and LAST_UPDATAED @@ -401,12 +401,12 @@ def test_patch_subresource(self): _db = self.connection[MONGO_DBNAME] # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices/%s' % diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index 1f50fd06a..fdd3511f4 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -210,12 +210,12 @@ def test_put_subresource(self): self.app.config['BANDWIDTH_SAVER'] = False # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, + {'$set': {'person': fake_contact_id}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices/%s' % @@ -236,17 +236,16 @@ def test_put_dbref_subresource(self): self.app.config['BANDWIDTH_SAVER'] = False # create random contact - fake_contact = self.random_contacts(1) - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update({'_id': ObjectId(self.invoice_id)}, - {'$set': { - 'person': fake_contact_id, - 'persondbref': - DBRef("contacts", - ObjectId(fake_contact_id))} - }) + _db.invoices.update_one( + {'_id': ObjectId(self.invoice_id)}, + {'$set': { + 'person': fake_contact_id, + 'persondbref': DBRef("contacts", + ObjectId(fake_contact_id))}}) # GET all invoices by new contact response, status = self.get('users/%s/invoices/%s' % diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 8bbadf421..f24568f64 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -30,9 +30,9 @@ def test_xml_leaf_escaping(self): # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] - fake_contact = self.random_contacts(1) - fake_contact[0]['ref'] = "12345 & 67890" - fake_contact_id = _db.contacts.insert(fake_contact)[0] + fake_contact = self.random_contacts(1)[0] + fake_contact['ref'] = "12345 & 67890" + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id r = self.test_client.get('%s/%s' % (self.known_resource_url, fake_contact_id), diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index bdff54511..3f8a2a5ea 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -34,7 +34,7 @@ 'cache_expires': 20, 'item_title': 'contact', 'additional_lookup': { - 'url': 'regex("[\w]+")', # to be unique field + 'url': r'regex("[\w]+")', # to be unique field 'field': 'ref' }, 'datasource': {'filter': {'username': {'$exists': False}}}, From c853fca2a17943899bd138fd42c39ccaffbfe093 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 27 Apr 2018 16:50:09 +0200 Subject: [PATCH 306/821] Drop Python 2.6 and Python 3.3 support --- .travis.yml | 2 -- CHANGES | 4 ++-- eve/methods/common.py | 7 +------ eve/render.py | 7 +------ eve/tests/methods/common.py | 6 +----- py26-requirements.txt | 3 --- setup.py | 17 ++++------------- tox.ini | 4 +--- 8 files changed, 10 insertions(+), 40 deletions(-) delete mode 100644 py26-requirements.txt diff --git a/.travis.yml b/.travis.yml index afb56f0b7..496426dfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,7 @@ language: python cache: pip script: tox python: - - 2.6 - 2.7 - - 3.3 - 3.4 - 3.5 - 3.6 diff --git a/CHANGES b/CHANGES index 4f5333e51..deeb53b51 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Python 2.6 and Python 3.3 are no longer supported. - Tests: finally acknowledge the existence of modern APIs for both Mongo and Python (get rid of most deprecation warnings). - New: ``before_aggregation`` and ``after_aggregation`` event hooks allow to @@ -18,7 +19,6 @@ Version 0.8 - New: Add suport for mongo's ``$box`` geo query operator. Closes #1122. - New: support for partial media requests. Clients can request partial file downloads by adding a ``Range`` header to their media request (Marsch Huynh). -- Python 2.6 is deprecated. This is the last release supporting Python 2.6, and you should upgrade to Python 3 as soon as possible. Closes #1129. - Fix: PUT behavior with User Restricted Resource Access. Ensure that, under every circumstance, users are unable to overwrite items owned by other users @@ -34,7 +34,7 @@ Version 0.8 sub-document fields. Closes #1123 (Luca Moretto). - Fix documentation typos (Olof Johansson) - Fix a changelog typo (kreynen). -- Update: bump Flask requirement to <=1.0. Closes #1111. +- Update: bump Flask requirement to <1.0. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You diff --git a/eve/methods/common.py b/eve/methods/common.py index 489f81d86..bc11cbf88 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -26,12 +26,7 @@ document_etag, parse_request from eve.versioning import get_data_version_relation_document, \ resolve_document_version - - -try: - from collections import Counter -except: - from backport_collections import Counter +from collections import Counter def get_document(resource, concurrency_check, original=None, diff --git a/eve/render.py b/eve/render.py index 436ad06d9..c9612509e 100644 --- a/eve/render.py +++ b/eve/render.py @@ -20,12 +20,7 @@ from eve.utils import date_to_str, date_to_rfc1123, config, \ debug_error_message, import_from_string from flask import make_response, request, Response, current_app as app, abort - -try: - from collections import OrderedDict # noqa -except ImportError: - # Python 2.6 needs this back-port - from backport_collections import OrderedDict +from collections import OrderedDict # noqa def raise_event(f): diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 471aa33a3..25cb8daca 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -11,11 +11,7 @@ from eve.tests.test_settings import MONGO_DBNAME from eve.utils import config -try: - from collections import OrderedDict # noqa -except ImportError: - # Python 2.6 needs this back-port - from backport_collections import OrderedDict +from collections import OrderedDict # noqa class TestSerializer(TestBase): diff --git a/py26-requirements.txt b/py26-requirements.txt deleted file mode 100644 index c0fe0c827..000000000 --- a/py26-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ --r requirements.txt -backport_collections==0.1 -importlib==1.0.4 \ No newline at end of file diff --git a/setup.py b/setup.py index d7e50da8b..96c0e659f 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +from collections import Counter, OrderedDict # noqa +import importlib + from setuptools import setup, find_packages DESCRIPTION = ("Python REST API for Humans.") @@ -17,16 +20,6 @@ 'pymongo>=3.5', ] -try: - from collections import Counter, OrderedDict # noqa - import importlib -except ImportError: - # Python 2.6 - install_requires.append('backport_collections') - install_requires.append('importlib==1.0.4') - install_requires.append('testfixtures<6.0.0') - - setup( name='Eve', version='0.8-dev', @@ -46,7 +39,7 @@ test_suite="eve.tests", install_requires=install_requires, tests_require=['redis', 'testfixtures'], - python_requires='>=2.6', + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', @@ -55,10 +48,8 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/tox.ini b/tox.ini index 8f980c00c..cc04d813c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py26,py27,py33,py34,py35,py36,pypy +envlist=py27,py34,py35,py36,pypy [testenv] commands=python setup.py test {posargs} @@ -10,9 +10,7 @@ basepython=python3 commands=flake8 --ignore=E731,E722,F821 eve {posargs} [tox:travis] -2.6 = py26 2.7 = py27 -3.3 = py33 3.4 = py34 3.5 = py35, flake8 3.6 = py36 From 66b15686cfa0cfcca2b7deabd277661a85a25400 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 4 May 2018 09:35:30 +0200 Subject: [PATCH 307/821] flake8 on all tox runs So we get a flake8 check even when running tests against a single python interpreter (which appears to be the standard when working locally). Will hopefully reduce the number of PRs with pep/flake issues. --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index cc04d813c..f1fe973f5 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,8 @@ basepython=python3 commands=flake8 --ignore=E731,E722,F821 eve {posargs} [tox:travis] -2.7 = py27 -3.4 = py34 -3.5 = py35, flake8 -3.6 = py36 -pypy = pypy +2.7 = py27,flake8 +3.4 = py34,flake8 +3.5 = py35,flake8 +3.6 = py36,flake8 +pypy = pypy,flake8 From 292d7c326c9838f3cf3619fcb9a736cfc719a366 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 4 May 2018 10:05:40 +0200 Subject: [PATCH 308/821] Run standard mongodb service on travis-ci --- .travis.yml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 496426dfc..db62b10c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,19 +10,8 @@ python: - pypy install: travis_retry pip install tox-travis services: - #- mongodb + - mongodb - redis-server before_script: - # work-around to make travis-ci working with mongod 3.4 - # https://github.com/travis-ci/travis-ci/issues/3694 - # https://github.com/travis-ci/apt-package-whitelist/issues/516 - - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.7.tgz -O /tmp/mongodb.tgz - - tar -xvf /tmp/mongodb.tgz - - mkdir /tmp/data - - ${PWD}/mongodb-linux-x86_64-3.4.7/bin/mongod --dbpath /tmp/data --bind_ip 127.0.0.1 --noauth &> /dev/null & - - until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done - sleep 15 - # timer is needed in order to get mongo to properly initialize on travis-ci - # See https://github.com/travis-ci/travis-ci/issues/1967#issuecomment-42008605 - - "${PWD}/mongodb-linux-x86_64-3.4.7/bin/mongo eve_test --eval 'db.createUser({\"user\": \"test_user\", \"pwd\": \"test_pw\", \"roles\": [\"readWrite\", \"dbAdmin\"]},{\"w\": \"majority\" , \"wtimeout\": 5000 })'" - #- mongo eve_test --eval 'db.addUser("test_user", "test_pw");' + - mongo eve_test --eval 'db.createUser({user:"test_user",pwd:"test_pw",roles:["readWrite"]});' From 3cd10aa1eec597aa0e4468e6bf9889550ff74612 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 4 May 2018 10:19:02 +0200 Subject: [PATCH 309/821] Replace obsolete tox:travis section in tox.ini --- tox.ini | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index f1fe973f5..441643762 100644 --- a/tox.ini +++ b/tox.ini @@ -9,9 +9,10 @@ deps=flake8 basepython=python3 commands=flake8 --ignore=E731,E722,F821 eve {posargs} -[tox:travis] -2.7 = py27,flake8 -3.4 = py34,flake8 -3.5 = py35,flake8 -3.6 = py36,flake8 -pypy = pypy,flake8 +[travis] +python = + 2.7: py27,flake8 + 3.4: py34,flake8 + 3.5: py35,flake8 + 3.6: py36,flake8 + pypy: pypy,flake8 From b80150e84d50bd6fdeb90ae15a09c7e2f4b14d8c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 4 May 2018 10:28:41 +0200 Subject: [PATCH 310/821] Add some minor trove classifiers --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 96c0e659f..67a133b4e 100755 --- a/setup.py +++ b/setup.py @@ -54,5 +54,8 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Software Development :: Libraries :: Python Modules', ], ) From c17e3d989f940fa7b7781ce527e60ef4da9eeecb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 5 May 2018 11:24:41 +0200 Subject: [PATCH 311/821] Fix obsolete Py26 and Py33 references in the docs --- .pytest_cache/v/cache/lastfailed | 789 +++++++++++++++++++++++++++++++ .pytest_cache/v/cache/nodeids | 779 ++++++++++++++++++++++++++++++ docs/index.rst | 2 +- docs/testing.rst | 11 +- 4 files changed, 1574 insertions(+), 7 deletions(-) create mode 100644 .pytest_cache/v/cache/lastfailed create mode 100644 .pytest_cache/v/cache/nodeids diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 000000000..e1520469e --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1,789 @@ +{ + "eve/tests/auth.py::TestBasicAuth::test_restricted_item_access": true, + "eve/tests/auth.py::TestBasicAuth::test_restricted_resource_access": true, + "eve/tests/auth.py::TestBasicAuth::test_rfc2617_response": true, + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_home_access": true, + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_item_access": true, + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_resource_access": true, + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_schema_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_ALLOWED_ROLES_does_not_change": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_item_roles_does_not_change": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_roles_does_not_change": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_home_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_item_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_media_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_resource_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_schema_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_bad_auth_class": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_custom_auth": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_home_public_methods": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_instanced_auth": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_item": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_resource": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_item": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_resource": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_home_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_item_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_resource_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_rfc2617_response": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_home_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_item_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_resource_access": true, + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_schema_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_ALLOWED_ROLES_does_not_change": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_item_roles_does_not_change": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_roles_does_not_change": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_home_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_item_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_media_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_resource_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_schema_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_bad_auth_class": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_custom_auth": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_home_public_methods": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_instanced_auth": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_item": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_resource": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_item": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_resource": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_home_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_item_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_resource_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_rfc2617_response": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_home_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_item_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_resource_access": true, + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_schema_access": true, + "eve/tests/auth.py::TestHMACAuth::test_ALLOWED_ROLES_does_not_change": true, + "eve/tests/auth.py::TestHMACAuth::test_allowed_item_roles_does_not_change": true, + "eve/tests/auth.py::TestHMACAuth::test_allowed_roles_does_not_change": true, + "eve/tests/auth.py::TestHMACAuth::test_authorized_home_access": true, + "eve/tests/auth.py::TestHMACAuth::test_authorized_item_access": true, + "eve/tests/auth.py::TestHMACAuth::test_authorized_media_access": true, + "eve/tests/auth.py::TestHMACAuth::test_authorized_resource_access": true, + "eve/tests/auth.py::TestHMACAuth::test_authorized_schema_access": true, + "eve/tests/auth.py::TestHMACAuth::test_bad_auth_class": true, + "eve/tests/auth.py::TestHMACAuth::test_custom_auth": true, + "eve/tests/auth.py::TestHMACAuth::test_home_public_methods": true, + "eve/tests/auth.py::TestHMACAuth::test_instanced_auth": true, + "eve/tests/auth.py::TestHMACAuth::test_post_resource_hmac_auth": true, + "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_item": true, + "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_resource": true, + "eve/tests/auth.py::TestHMACAuth::test_public_methods_item": true, + "eve/tests/auth.py::TestHMACAuth::test_public_methods_resource": true, + "eve/tests/auth.py::TestHMACAuth::test_restricted_home_access": true, + "eve/tests/auth.py::TestHMACAuth::test_restricted_item_access": true, + "eve/tests/auth.py::TestHMACAuth::test_restricted_resource_access": true, + "eve/tests/auth.py::TestHMACAuth::test_rfc2617_response": true, + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_home_access": true, + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_item_access": true, + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_resource_access": true, + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_schema_access": true, + "eve/tests/auth.py::TestResourceAuth::test_resource_only_auth": true, + "eve/tests/auth.py::TestTokenAuth::test_ALLOWED_ROLES_does_not_change": true, + "eve/tests/auth.py::TestTokenAuth::test_allowed_item_roles_does_not_change": true, + "eve/tests/auth.py::TestTokenAuth::test_allowed_roles_does_not_change": true, + "eve/tests/auth.py::TestTokenAuth::test_authorized_home_access": true, + "eve/tests/auth.py::TestTokenAuth::test_authorized_item_access": true, + "eve/tests/auth.py::TestTokenAuth::test_authorized_media_access": true, + "eve/tests/auth.py::TestTokenAuth::test_authorized_resource_access": true, + "eve/tests/auth.py::TestTokenAuth::test_authorized_schema_access": true, + "eve/tests/auth.py::TestTokenAuth::test_bad_auth_class": true, + "eve/tests/auth.py::TestTokenAuth::test_custom_auth": true, + "eve/tests/auth.py::TestTokenAuth::test_home_public_methods": true, + "eve/tests/auth.py::TestTokenAuth::test_instanced_auth": true, + "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_item": true, + "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_resource": true, + "eve/tests/auth.py::TestTokenAuth::test_public_methods_item": true, + "eve/tests/auth.py::TestTokenAuth::test_public_methods_resource": true, + "eve/tests/auth.py::TestTokenAuth::test_restricted_home_access": true, + "eve/tests/auth.py::TestTokenAuth::test_restricted_item_access": true, + "eve/tests/auth.py::TestTokenAuth::test_restricted_resource_access": true, + "eve/tests/auth.py::TestTokenAuth::test_rfc2617_response": true, + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_home_access": true, + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_item_access": true, + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_resource_access": true, + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_schema_access": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_collection_get_public": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_delete": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_delete_item": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_filter_by_auth_field_id": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_get": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_criteria": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_id": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_item_get_public": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_patch": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_post": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_post_bandwidth_saver_off_resource_auth": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_post_resource_auth": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_put": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_put_bandwidth_saver_off_resource_auth": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_put_resource_auth": true, + "eve/tests/auth.py::TestUserRestrictedAccess::test_unique_to_user_on_post": true, + "eve/tests/config.py::TestConfig::test_allow_unknown_with_soft_delete": true, + "eve/tests/config.py::TestConfig::test_auth_field_as_custom_idfield": true, + "eve/tests/config.py::TestConfig::test_auth_field_as_idfield": true, + "eve/tests/config.py::TestConfig::test_create_indexes": true, + "eve/tests/config.py::TestConfig::test_custom_datalayer": true, + "eve/tests/config.py::TestConfig::test_custom_error_handlers": true, + "eve/tests/config.py::TestConfig::test_custom_import_name": true, + "eve/tests/config.py::TestConfig::test_custom_kwargs": true, + "eve/tests/config.py::TestConfig::test_custom_validator": true, + "eve/tests/config.py::TestConfig::test_datasource": true, + "eve/tests/config.py::TestConfig::test_default_datalayer": true, + "eve/tests/config.py::TestConfig::test_default_import_name": true, + "eve/tests/config.py::TestConfig::test_default_settings": true, + "eve/tests/config.py::TestConfig::test_default_validator": true, + "eve/tests/config.py::TestConfig::test_existing_env_config": true, + "eve/tests/config.py::TestConfig::test_mongodb_settings": true, + "eve/tests/config.py::TestConfig::test_oplog_config": true, + "eve/tests/config.py::TestConfig::test_pretty_resource_urls": true, + "eve/tests/config.py::TestConfig::test_regexconverter": true, + "eve/tests/config.py::TestConfig::test_register_resource": true, + "eve/tests/config.py::TestConfig::test_set_defaults": true, + "eve/tests/config.py::TestConfig::test_set_schema_defaults": true, + "eve/tests/config.py::TestConfig::test_settings_as_dict": true, + "eve/tests/config.py::TestConfig::test_unexisting_env_config": true, + "eve/tests/config.py::TestConfig::test_url_helpers": true, + "eve/tests/config.py::TestConfig::test_url_rules": true, + "eve/tests/config.py::TestConfig::test_validate_datecreated_in_schema": true, + "eve/tests/config.py::TestConfig::test_validate_domain_struct": true, + "eve/tests/config.py::TestConfig::test_validate_invalid_field_names": true, + "eve/tests/config.py::TestConfig::test_validate_item_methods": true, + "eve/tests/config.py::TestConfig::test_validate_lastupdated_in_schema": true, + "eve/tests/config.py::TestConfig::test_validate_resource_methods": true, + "eve/tests/config.py::TestConfig::test_validate_roles": true, + "eve/tests/config.py::TestConfig::test_validate_schema": true, + "eve/tests/config.py::TestConfig::test_validate_schema_item_methods": true, + "eve/tests/config.py::TestConfig::test_validate_schema_methods": true, + "eve/tests/endpoints.py::TestCustomConverters::test_delete_uuid": true, + "eve/tests/endpoints.py::TestCustomConverters::test_get_uuid": true, + "eve/tests/endpoints.py::TestCustomConverters::test_patch_uuid": true, + "eve/tests/endpoints.py::TestCustomConverters::test_post_uuid": true, + "eve/tests/endpoints.py::TestCustomConverters::test_put_uuid": true, + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix": true, + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_post_internal": true, + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version": true, + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version_hateoas_links": true, + "eve/tests/endpoints.py::TestEndPoints::test_api_version": true, + "eve/tests/endpoints.py::TestEndPoints::test_homepage": true, + "eve/tests/endpoints.py::TestEndPoints::test_homepage_does_not_have_internal_resources": true, + "eve/tests/endpoints.py::TestEndPoints::test_internal_endpoint": true, + "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_additional_lookup": true, + "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_id": true, + "eve/tests/endpoints.py::TestEndPoints::test_item_self_link": true, + "eve/tests/endpoints.py::TestEndPoints::test_nested_endpoint": true, + "eve/tests/endpoints.py::TestEndPoints::test_oplog_endpoint": true, + "eve/tests/endpoints.py::TestEndPoints::test_resource_endpoint": true, + "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint": true, + "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint_does_not_attempt_callable_serialization": true, + "eve/tests/endpoints.py::TestEndPoints::test_unknown_endpoints": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_config": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_mongo_url": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_auth_params_provided": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_options": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_port": true, + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_valid_port": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_get_media_can_leverage_projection": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_base_url": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete_projection": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_errors": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch_null": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_excluded_file_in_result": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended_excluded_file_in_result": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_put": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_return_url": true, + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_partial_media": true, + "eve/tests/io/media.py::TestMediaStorage::test_base_media_storage": true, + "eve/tests/io/mongo.py::TestMongoDriver::test_combine_queries": true, + "eve/tests/io/mongo.py::TestMongoDriver::test_delete_returns_status": true, + "eve/tests/io/mongo.py::TestMongoDriver::test_get_value_from_query": true, + "eve/tests/io/mongo.py::TestMongoDriver::test_json_encoder_class": true, + "eve/tests/io/mongo.py::TestMongoDriver::test_query_contains_field": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_dependencies_with_defaults": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_feature_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_feature_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_geojson_not_compilant": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_geometry_not_compilant": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_not_compilant": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_multilinestring_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_multipoint_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_multipolygon_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_point_coordinates_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_point_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_point_integer_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_point_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_success": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_reject_invalid_schema": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_unique_fail": true, + "eve/tests/io/mongo.py::TestMongoValidator::test_unique_success": true, + "eve/tests/io/mongo.py::TestPythonParser::test_And_BoolOp": true, + "eve/tests/io/mongo.py::TestPythonParser::test_Attribute": true, + "eve/tests/io/mongo.py::TestPythonParser::test_Eq": true, + "eve/tests/io/mongo.py::TestPythonParser::test_Gt": true, + "eve/tests/io/mongo.py::TestPythonParser::test_GtE": true, + "eve/tests/io/mongo.py::TestPythonParser::test_Lt": true, + "eve/tests/io/mongo.py::TestPythonParser::test_LtE": true, + "eve/tests/io/mongo.py::TestPythonParser::test_NotEq": true, + "eve/tests/io/mongo.py::TestPythonParser::test_ObjectId_Call": true, + "eve/tests/io/mongo.py::TestPythonParser::test_Or_BoolOp": true, + "eve/tests/io/mongo.py::TestPythonParser::test_bad_Expr": true, + "eve/tests/io/mongo.py::TestPythonParser::test_datetime_Call": true, + "eve/tests/io/mongo.py::TestPythonParser::test_nested_BoolOp": true, + "eve/tests/io/mongo.py::TestPythonParser::test_unparsed_statement": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_create_index_with_mongo_uri_and_prefix": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_delete_multidb": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_get_multidb": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_patch_multidb": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_post_multidb": true, + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_put_multidb": true, + "eve/tests/io/multi_mongo.py::TestMultiMongoAuth::test_get_multidb": true, + "eve/tests/logging.py::TestUtils::test_logging_info": true, + "eve/tests/methods/common.py::TestNormalizeDottedFields::test_normalize_dotted_fields": true, + "eve/tests/methods/common.py::TestOpLogEndpointDisabled::test_post_oplog": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_delete_oplog": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_oplog_hook": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_patch_oplog": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_basic_auth": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_hmac_auth": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_token_auth": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog_does_not_alter_document": true, + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_soft_delete_oplog": true, + "eve/tests/methods/common.py::TestSerializer::test_dbref_serialize_lists_of_lists": true, + "eve/tests/methods/common.py::TestSerializer::test_mongo_serializes": true, + "eve/tests/methods/common.py::TestSerializer::test_non_blocking_on_simple_field_serialization_exception": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_alongside_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_boolean": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_schema_of_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_typesavers": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_nested_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_typesavers": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_list_alongside_x_of_rules": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_lists_of_lists": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_null_dictionary": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_null_list": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_number": true, + "eve/tests/methods/common.py::TestSerializer::test_serialize_subdocument": true, + "eve/tests/methods/common.py::TestTickets::test_ticket_681": true, + "eve/tests/methods/delete.py::TestDelete::test_bulk_delete_id_field": true, + "eve/tests/methods/delete.py::TestDelete::test_delete": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_custom_idfield": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_different_resource": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_empty_resource": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_different_resource": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_write_concern": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_bad_etag": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_missing": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_non_existant": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource_with_override": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_subresource": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_subresource_item": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_unknown_item": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_with_post_override": true, + "eve/tests/methods/delete.py::TestDelete::test_delete_write_concern": true, + "eve/tests/methods/delete.py::TestDelete::test_deleteitem_internal": true, + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_disabled_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_missing_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestDelete::test_unknown_resource": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item_contacts": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource_contacts": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item_contacts": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_resource_contacts": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_resource": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_resource": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_dynamic_filter": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_resource": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_item": true, + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_resource": true, + "eve/tests/methods/delete.py::TestResourceSpecificSoftDelete::test_resource_specific_softdelete": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_bulk_delete_id_field": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_custom_idfield": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_different_resource": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_empty_resource": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_different_resource": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_write_concern": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_bad_etag": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_missing": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_non_existant": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource_with_override": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource_item": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_unknown_item": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_with_post_override": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_write_concern": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_deleteitem_internal": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_exclude_soft_deleted_documents_from_unique_checks": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_exclusive_projection": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_disabled_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_missing_enforce_ifmatch_disabled": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_multiple_softdelete": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_restore_softdeleted": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_caching": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_datalayer": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_db_fields": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_deleted_field": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_show_deleted": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_embedded_doc": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_get_response_skips_embedded_expansion": true, + "eve/tests/methods/delete.py::TestSoftDelete::test_unknown_resource": true, + "eve/tests/methods/get.py::TestEvents::test_get_after_aggregation_hook": true, + "eve/tests/methods/get.py::TestEvents::test_get_before_aggregation_hook": true, + "eve/tests/methods/get.py::TestEvents::test_on_fetched_item": true, + "eve/tests/methods/get.py::TestEvents::test_on_fetched_item_contacts": true, + "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource": true, + "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource_contacts": true, + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_item": true, + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_resource": true, + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_homepage": true, + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_item": true, + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_resource": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_item": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_resource": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_item_dynamic_filter": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter_12_chr_nonunicode_string": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_item": true, + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_resource": true, + "eve/tests/methods/get.py::TestGet::test_cache_control": true, + "eve/tests/methods/get.py::TestGet::test_cursor_extra_find": true, + "eve/tests/methods/get.py::TestGet::test_documents_missing_standard_date_fields": true, + "eve/tests/methods/get.py::TestGet::test_expires": true, + "eve/tests/methods/get.py::TestGet::test_get": true, + "eve/tests/methods/get.py::TestGet::test_get_aggregation_endpoint": true, + "eve/tests/methods/get.py::TestGet::test_get_aggregation_pagination": true, + "eve/tests/methods/get.py::TestGet::test_get_aggregation_parsing": true, + "eve/tests/methods/get.py::TestGet::test_get_aggregation_with_lists": true, + "eve/tests/methods/get.py::TestGet::test_get_allowed_filters_operators": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_auto_document_fields": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_embedded": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_hateoas_links": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_idfield": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_items": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_links": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_max_results": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_page": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_params": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_projection": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_sort": true, + "eve/tests/methods/get.py::TestGet::test_get_custom_where": true, + "eve/tests/methods/get.py::TestGet::test_get_default_sort": true, + "eve/tests/methods/get.py::TestGet::test_get_embedded": true, + "eve/tests/methods/get.py::TestGet::test_get_embedded_media": true, + "eve/tests/methods/get.py::TestGet::test_get_embedded_media_validate_rest_of_fields": true, + "eve/tests/methods/get.py::TestGet::test_get_empty_resource": true, + "eve/tests/methods/get.py::TestGet::test_get_idfield_doesnt_exist": true, + "eve/tests/methods/get.py::TestGet::test_get_ifmatch_disabled": true, + "eve/tests/methods/get.py::TestGet::test_get_ims_empty_resource": true, + "eve/tests/methods/get.py::TestGet::test_get_internal_page": true, + "eve/tests/methods/get.py::TestGet::test_get_invalid_idfield_cors": true, + "eve/tests/methods/get.py::TestGet::test_get_invalid_sort_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_invalid_where_fields": true, + "eve/tests/methods/get.py::TestGet::test_get_invalid_where_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_lookup_field_as_string": true, + "eve/tests/methods/get.py::TestGet::test_get_max_results": true, + "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist": true, + "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist_nested": true, + "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_unvalidated": true, + "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_validated": true, + "eve/tests/methods/get.py::TestGet::test_get_nested_resource": true, + "eve/tests/methods/get.py::TestGet::test_get_page": true, + "eve/tests/methods/get.py::TestGet::test_get_pagination_no_documents": true, + "eve/tests/methods/get.py::TestGet::test_get_paging_disabled_no_args": true, + "eve/tests/methods/get.py::TestGet::test_get_perform_count_on_pagination_disabled": true, + "eve/tests/methods/get.py::TestGet::test_get_projection": true, + "eve/tests/methods/get.py::TestGet::test_get_projection_consistent_etag": true, + "eve/tests/methods/get.py::TestGet::test_get_projection_noschema": true, + "eve/tests/methods/get.py::TestGet::test_get_projection_subdocument": true, + "eve/tests/methods/get.py::TestGet::test_get_query_bitwise_query_operators": true, + "eve/tests/methods/get.py::TestGet::test_get_query_in_links": true, + "eve/tests/methods/get.py::TestGet::test_get_reference_embedded_in_subdocuments": true, + "eve/tests/methods/get.py::TestGet::test_get_resource_title": true, + "eve/tests/methods/get.py::TestGet::test_get_same_collection_different_resource": true, + "eve/tests/methods/get.py::TestGet::test_get_server_exclude_projection_can_project_others": true, + "eve/tests/methods/get.py::TestGet::test_get_server_exlcude_projection_can_sniff": true, + "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_block_sniff": true, + "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_can_exclude": true, + "eve/tests/methods/get.py::TestGet::test_get_sort_comma_delimited_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_sort_disabled": true, + "eve/tests/methods/get.py::TestGet::test_get_sort_mongo_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_static_projection": true, + "eve/tests/methods/get.py::TestGet::test_get_subresource": true, + "eve/tests/methods/get.py::TestGet::test_get_subresource_with_custom_idfield": true, + "eve/tests/methods/get.py::TestGet::test_get_total_count_header": true, + "eve/tests/methods/get.py::TestGet::test_get_where_allowed_filters": true, + "eve/tests/methods/get.py::TestGet::test_get_where_disabled": true, + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_combined_date": true, + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_objectid_as_string": true, + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax": true, + "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax1": true, + "eve/tests/methods/get.py::TestGet::test_get_with_post_override": true, + "eve/tests/methods/get.py::TestGetItem::test_cache_control": true, + "eve/tests/methods/get.py::TestGetItem::test_disallowed_getitem": true, + "eve/tests/methods/get.py::TestGetItem::test_expires": true, + "eve/tests/methods/get.py::TestGetItem::test_get_with_post_override": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id_different_resource": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_integer": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_different_resource": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_self_href": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_custom_auto_document_fields": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_embedded": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_if_modified_since": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_if_none_match": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled_if_mod_since": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_internal_by_id": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_lookup_field_as_string": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_missing_standard_date_fields": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_noschema": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_projection": true, + "eve/tests/methods/get.py::TestGetItem::test_getitem_with_custom_idfield": true, + "eve/tests/methods/get.py::TestGetItem::test_subresource_getitem": true, + "eve/tests/methods/get.py::TestHead::test_head_home": true, + "eve/tests/methods/get.py::TestHead::test_head_item": true, + "eve/tests/methods/get.py::TestHead::test_head_resource": true, + "eve/tests/methods/patch.py::TestEvents::test_on_PATCH_dynamic_filter": true, + "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH": true, + "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH_contacts": true, + "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH": true, + "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH_contacts": true, + "eve/tests/methods/patch.py::TestEvents::test_on_update": true, + "eve/tests/methods/patch.py::TestEvents::test_on_update_contacts": true, + "eve/tests/methods/patch.py::TestEvents::test_on_updated": true, + "eve/tests/methods/patch.py::TestEvents::test_on_updated_contacts": true, + "eve/tests/methods/patch.py::TestPatch::test_by_name": true, + "eve/tests/methods/patch.py::TestPatch::test_id_field_in_document_fails": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled_enforce_ifmatch_disabled": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing": true, + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing_enforce_ifmatch_disabled": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_allow_unknown": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_bandwidth_saver": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_custom_idfield": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_datetime": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_on_origin_document": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_value_on_origin_document": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_dict": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header_enforce_ifmatch_disabled": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_integer": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_internal": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_list": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_list_as_array": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default_with_post_override": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_standard_date_fields": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_multiple_fields": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_nested": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_not_overwritten": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_nullable_missing": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_null_objectid": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_objectid": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_readonly_field_with_previous_document": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_referential_integrity": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_rows": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_string": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_subresource": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_to_resource_endpoint": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_type_coercion": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_with_post_override": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_fail": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_success": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded": true, + "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded_number_serialization": true, + "eve/tests/methods/patch.py::TestPatch::test_readonly_resource": true, + "eve/tests/methods/patch.py::TestPatch::test_unique_value": true, + "eve/tests/methods/patch.py::TestPatch::test_unknown_id": true, + "eve/tests/methods/patch.py::TestPatch::test_unknown_id_different_resource": true, + "eve/tests/methods/post.py::TestEvents::test_on_POST_post_resource": true, + "eve/tests/methods/post.py::TestEvents::test_on_insert": true, + "eve/tests/methods/post.py::TestEvents::test_on_insert_contacts": true, + "eve/tests/methods/post.py::TestEvents::test_on_inserted": true, + "eve/tests/methods/post.py::TestEvents::test_on_inserted_contacts": true, + "eve/tests/methods/post.py::TestEvents::test_on_post_POST": true, + "eve/tests/methods/post.py::TestEvents::test_on_pre_POST": true, + "eve/tests/methods/post.py::TestEvents::test_on_pre_POST_contacts": true, + "eve/tests/methods/post.py::TestPost::test_custom_date_updated": true, + "eve/tests/methods/post.py::TestPost::test_custom_etag_update_date": true, + "eve/tests/methods/post.py::TestPost::test_custom_issues": true, + "eve/tests/methods/post.py::TestPost::test_custom_status": true, + "eve/tests/methods/post.py::TestPost::test_dbref_post_referential_integrity": true, + "eve/tests/methods/post.py::TestPost::test_id_field_included_with_document": true, + "eve/tests/methods/post.py::TestPost::test_multi_post_invalid": true, + "eve/tests/methods/post.py::TestPost::test_multi_post_valid": true, + "eve/tests/methods/post.py::TestPost::test_post_allow_unknown": true, + "eve/tests/methods/post.py::TestPost::test_post_alternative_payload": true, + "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_media_list": true, + "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_multiple_keys": true, + "eve/tests/methods/post.py::TestPost::test_post_auto_create_lists": true, + "eve/tests/methods/post.py::TestPost::test_post_bandwidth_saver": true, + "eve/tests/methods/post.py::TestPost::test_post_bulk_insert_on_disabled_bulk": true, + "eve/tests/methods/post.py::TestPost::test_post_custom_idfield": true, + "eve/tests/methods/post.py::TestPost::test_post_custom_json_content_type": true, + "eve/tests/methods/post.py::TestPost::test_post_datetime": true, + "eve/tests/methods/post.py::TestPost::test_post_decimal_number_fail": true, + "eve/tests/methods/post.py::TestPost::test_post_decimal_number_success": true, + "eve/tests/methods/post.py::TestPost::test_post_default_value": true, + "eve/tests/methods/post.py::TestPost::test_post_default_value_none": true, + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_default": true, + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_subdocuments": true, + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_values": true, + "eve/tests/methods/post.py::TestPost::test_post_dependency_required_fields": true, + "eve/tests/methods/post.py::TestPost::test_post_dict": true, + "eve/tests/methods/post.py::TestPost::test_post_duplicate_key": true, + "eve/tests/methods/post.py::TestPost::test_post_empty_bulk_insert": true, + "eve/tests/methods/post.py::TestPost::test_post_empty_resource": true, + "eve/tests/methods/post.py::TestPost::test_post_error_as_list": true, + "eve/tests/methods/post.py::TestPost::test_post_float_zero": true, + "eve/tests/methods/post.py::TestPost::test_post_ifmatch_disabled": true, + "eve/tests/methods/post.py::TestPost::test_post_integer": true, + "eve/tests/methods/post.py::TestPost::test_post_integer_zero": true, + "eve/tests/methods/post.py::TestPost::test_post_internal": true, + "eve/tests/methods/post.py::TestPost::test_post_internal_skip_validation": true, + "eve/tests/methods/post.py::TestPost::test_post_keyschema_dict": true, + "eve/tests/methods/post.py::TestPost::test_post_list": true, + "eve/tests/methods/post.py::TestPost::test_post_list_as_array": true, + "eve/tests/methods/post.py::TestPost::test_post_list_fixed_len": true, + "eve/tests/methods/post.py::TestPost::test_post_list_of_objectid": true, + "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_off": true, + "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_on": true, + "eve/tests/methods/post.py::TestPost::test_post_nested": true, + "eve/tests/methods/post.py::TestPost::test_post_nested_dict_objectid": true, + "eve/tests/methods/post.py::TestPost::test_post_null_objectid": true, + "eve/tests/methods/post.py::TestPost::test_post_objectid": true, + "eve/tests/methods/post.py::TestPost::test_post_readonly_field_with_default": true, + "eve/tests/methods/post.py::TestPost::test_post_readonly_in_dict": true, + "eve/tests/methods/post.py::TestPost::test_post_referential_integrity": true, + "eve/tests/methods/post.py::TestPost::test_post_referential_integrity_list": true, + "eve/tests/methods/post.py::TestPost::test_post_rows": true, + "eve/tests/methods/post.py::TestPost::test_post_string": true, + "eve/tests/methods/post.py::TestPost::test_post_to_item_endpoint": true, + "eve/tests/methods/post.py::TestPost::test_post_type_coercion": true, + "eve/tests/methods/post.py::TestPost::test_post_valueschema_dict": true, + "eve/tests/methods/post.py::TestPost::test_post_valueschema_with_objectid": true, + "eve/tests/methods/post.py::TestPost::test_post_with_content_type_charset": true, + "eve/tests/methods/post.py::TestPost::test_post_with_excluded_response_fields": true, + "eve/tests/methods/post.py::TestPost::test_post_with_extra_response_fields": true, + "eve/tests/methods/post.py::TestPost::test_post_with_get_override": true, + "eve/tests/methods/post.py::TestPost::test_post_with_relation_to_custom_idfield": true, + "eve/tests/methods/post.py::TestPost::test_post_write_concern": true, + "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded": true, + "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded_number_serialization": true, + "eve/tests/methods/post.py::TestPost::test_readonly_resource": true, + "eve/tests/methods/post.py::TestPost::test_subresource": true, + "eve/tests/methods/post.py::TestPost::test_subresource_required_ref": true, + "eve/tests/methods/post.py::TestPost::test_unknown_resource": true, + "eve/tests/methods/post.py::TestPost::test_validation_error": true, + "eve/tests/methods/put.py::TestEvents::test_on_post_PUT": true, + "eve/tests/methods/put.py::TestEvents::test_on_post_PUT_contacts": true, + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT": true, + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_contacts": true, + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_dynamic_filter": true, + "eve/tests/methods/put.py::TestEvents::test_on_replace": true, + "eve/tests/methods/put.py::TestEvents::test_on_replace_contacts": true, + "eve/tests/methods/put.py::TestEvents::test_on_replaced": true, + "eve/tests/methods/put.py::TestEvents::test_on_replaced_contacts": true, + "eve/tests/methods/put.py::TestPut::test_allow_unknown": true, + "eve/tests/methods/put.py::TestPut::test_by_name": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled_enforce_ifmatch_disabled": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_missing": true, + "eve/tests/methods/put.py::TestPut::test_ifmatch_missing_enforce_ifmatch_disabled": true, + "eve/tests/methods/put.py::TestPut::test_put_bandwidth_saver": true, + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document": true, + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_fails_on_mismatching_id": true, + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_with_url_as_id": true, + "eve/tests/methods/put.py::TestPut::test_put_custom_idfield": true, + "eve/tests/methods/put.py::TestPut::test_put_dbref_subresource": true, + "eve/tests/methods/put.py::TestPut::test_put_default_value": true, + "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_default": true, + "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_wrong_value": true, + "eve/tests/methods/put.py::TestPut::test_put_etag_header": true, + "eve/tests/methods/put.py::TestPut::test_put_etag_header_enforce_ifmatch_disabled": true, + "eve/tests/methods/put.py::TestPut::test_put_internal": true, + "eve/tests/methods/put.py::TestPut::test_put_internal_skip_validation": true, + "eve/tests/methods/put.py::TestPut::test_put_nested": true, + "eve/tests/methods/put.py::TestPut::test_put_readonly_value_different": true, + "eve/tests/methods/put.py::TestPut::test_put_readonly_value_same": true, + "eve/tests/methods/put.py::TestPut::test_put_referential_integrity": true, + "eve/tests/methods/put.py::TestPut::test_put_referential_integrity_list": true, + "eve/tests/methods/put.py::TestPut::test_put_returns_404_on_unexisting_document": true, + "eve/tests/methods/put.py::TestPut::test_put_string": true, + "eve/tests/methods/put.py::TestPut::test_put_subresource": true, + "eve/tests/methods/put.py::TestPut::test_put_to_resource_endpoint": true, + "eve/tests/methods/put.py::TestPut::test_put_type_coercion": true, + "eve/tests/methods/put.py::TestPut::test_put_with_post_override": true, + "eve/tests/methods/put.py::TestPut::test_put_write_concern_fail": true, + "eve/tests/methods/put.py::TestPut::test_put_write_concern_success": true, + "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded": true, + "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded_number_serialization": true, + "eve/tests/methods/put.py::TestPut::test_readonly_resource": true, + "eve/tests/methods/put.py::TestPut::test_unique_value": true, + "eve/tests/methods/ratelimit.py::TestRateLimit::test_noratelimits": true, + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_home": true, + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_item": true, + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_resource": true, + "eve/tests/renders.py::TestRenders::test_CORS": true, + "eve/tests/renders.py::TestRenders::test_CORS_MAX_AGE": true, + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS": true, + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_item": true, + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_resources": true, + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_schema": true, + "eve/tests/renders.py::TestRenders::test_CORS_regex": true, + "eve/tests/renders.py::TestRenders::test_default_render": true, + "eve/tests/renders.py::TestRenders::test_json_disabled": true, + "eve/tests/renders.py::TestRenders::test_json_keys_sorted": true, + "eve/tests/renders.py::TestRenders::test_json_render": true, + "eve/tests/renders.py::TestRenders::test_json_xml_disabled": true, + "eve/tests/renders.py::TestRenders::test_jsonp_enabled": true, + "eve/tests/renders.py::TestRenders::test_unknown_render": true, + "eve/tests/renders.py::TestRenders::test_xml_disabled": true, + "eve/tests/renders.py::TestRenders::test_xml_leaf_escaping": true, + "eve/tests/renders.py::TestRenders::test_xml_ordered_nodes": true, + "eve/tests/renders.py::TestRenders::test_xml_render": true, + "eve/tests/renders.py::TestRenders::test_xml_url_escaping": true, + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage": true, + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage_reply": true, + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_item": true, + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_resource": true, + "eve/tests/response.py::TestNoHateoas::test_patch_no_hateoas": true, + "eve/tests/response.py::TestNoHateoas::test_post_no_hateoas": true, + "eve/tests/response.py::TestResponse::test_response_data": true, + "eve/tests/response.py::TestResponse::test_response_object": true, + "eve/tests/response.py::TestResponse::test_response_pretty": true, + "eve/tests/utils.py::TestUtils::test_date_to_str": true, + "eve/tests/utils.py::TestUtils::test_debug_error_message": true, + "eve/tests/utils.py::TestUtils::test_document_etag": true, + "eve/tests/utils.py::TestUtils::test_document_etag_ignore_fields": true, + "eve/tests/utils.py::TestUtils::test_extract_key_values": true, + "eve/tests/utils.py::TestUtils::test_import_from_string": true, + "eve/tests/utils.py::TestUtils::test_parse_request_if_match": true, + "eve/tests/utils.py::TestUtils::test_parse_request_if_modified_since": true, + "eve/tests/utils.py::TestUtils::test_parse_request_if_none_match": true, + "eve/tests/utils.py::TestUtils::test_parse_request_max_results": true, + "eve/tests/utils.py::TestUtils::test_parse_request_max_results_disabled_pagination": true, + "eve/tests/utils.py::TestUtils::test_parse_request_page": true, + "eve/tests/utils.py::TestUtils::test_parse_request_sort": true, + "eve/tests/utils.py::TestUtils::test_parse_request_where": true, + "eve/tests/utils.py::TestUtils::test_querydef": true, + "eve/tests/utils.py::TestUtils::test_str_to_date": true, + "eve/tests/utils.py::TestUtils::test_validate_filters": true, + "eve/tests/utils.py::TestUtils::test_weak_date": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_automatic_fields": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_delete": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_deleteitem": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_get": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_projection": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all_projection": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_bad_format": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_diffs": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_modified_since": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_none_match": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_pagination": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_unknown": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_multi_post": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item_contacts": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_patch": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_post": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_put": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_referential_integrity": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete_version_db_fields": true, + "eve/tests/versioning.py::TestCompleteVersioning::test_version_control_the_unkown": true, + "eve/tests/versioning.py::TestLateVersioning::test_datasource": true, + "eve/tests/versioning.py::TestLateVersioning::test_delete": true, + "eve/tests/versioning.py::TestLateVersioning::test_deleteitem": true, + "eve/tests/versioning.py::TestLateVersioning::test_embedded": true, + "eve/tests/versioning.py::TestLateVersioning::test_get": true, + "eve/tests/versioning.py::TestLateVersioning::test_getitem": true, + "eve/tests/versioning.py::TestLateVersioning::test_patch": true, + "eve/tests/versioning.py::TestLateVersioning::test_put": true, + "eve/tests/versioning.py::TestLateVersioning::test_referential_integrity": true, + "eve/tests/versioning.py::TestLateVersioning::test_softdelete": true, + "eve/tests/versioning.py::TestPartialVersioning::test_get": true, + "eve/tests/versioning.py::TestPartialVersioning::test_getitem": true, + "eve/tests/versioning.py::TestPartialVersioning::test_multi_post": true, + "eve/tests/versioning.py::TestPartialVersioning::test_patch": true, + "eve/tests/versioning.py::TestPartialVersioning::test_post": true, + "eve/tests/versioning.py::TestPartialVersioning::test_put": true, + "eve/tests/versioning.py::TestPartialVersioning::test_version_control_the_unkown": true, + "eve/tests/versioning.py::TestVersionedDataRelation::test_embedded": true, + "eve/tests/versioning.py::TestVersionedDataRelation::test_referential_integrity": true, + "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_data_relation_validation": true, + "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_embedded": true, + "eve/tests/versioning.py::TestVersionedDataRelationCustomField::test_referential_integrity": true, + "eve/tests/versioning.py::TestVersionedDataRelationUnversionedField::test_referential_integrity": true, + "eve/tests/versioning.py::TestVersioningWithCustomIdField::test_getitem": true, + "tests/__init__.py": true, + "tests/auth.py": true, + "tests/config.py": true, + "tests/endpoints.py": true, + "tests/io/__init__.py": true, + "tests/io/flask_pymongo.py": true, + "tests/io/media.py": true, + "tests/io/mongo.py": true, + "tests/io/multi_mongo.py": true, + "tests/logging.py": true, + "tests/methods/__init__.py": true, + "tests/methods/common.py": true, + "tests/methods/delete.py": true, + "tests/methods/get.py": true, + "tests/methods/patch.py": true, + "tests/methods/post.py": true, + "tests/methods/put.py": true, + "tests/methods/ratelimit.py": true, + "tests/renders.py": true, + "tests/response.py": true, + "tests/test_prefix.py": true, + "tests/test_prefix_version.py": true, + "tests/test_settings.py": true, + "tests/test_settings_env.py": true, + "tests/test_version.py": true, + "tests/utils.py": true, + "tests/versioning.py": true +} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 000000000..83c7d1a54 --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,779 @@ +[ + "eve/tests/auth.py::TestBasicAuth::test_ALLOWED_ROLES_does_not_change", + "eve/tests/auth.py::TestBasicAuth::test_allowed_item_roles_does_not_change", + "eve/tests/auth.py::TestBasicAuth::test_allowed_roles_does_not_change", + "eve/tests/auth.py::TestBasicAuth::test_authorized_home_access", + "eve/tests/auth.py::TestBasicAuth::test_authorized_item_access", + "eve/tests/auth.py::TestBasicAuth::test_authorized_media_access", + "eve/tests/auth.py::TestBasicAuth::test_authorized_resource_access", + "eve/tests/auth.py::TestBasicAuth::test_authorized_schema_access", + "eve/tests/auth.py::TestBasicAuth::test_bad_auth_class", + "eve/tests/auth.py::TestBasicAuth::test_custom_auth", + "eve/tests/auth.py::TestBasicAuth::test_home_public_methods", + "eve/tests/auth.py::TestBasicAuth::test_instanced_auth", + "eve/tests/auth.py::TestBasicAuth::test_public_methods_but_locked_item", + "eve/tests/auth.py::TestBasicAuth::test_public_methods_but_locked_resource", + "eve/tests/auth.py::TestBasicAuth::test_public_methods_item", + "eve/tests/auth.py::TestBasicAuth::test_public_methods_resource", + "eve/tests/auth.py::TestBasicAuth::test_restricted_home_access", + "eve/tests/auth.py::TestBasicAuth::test_restricted_item_access", + "eve/tests/auth.py::TestBasicAuth::test_restricted_resource_access", + "eve/tests/auth.py::TestBasicAuth::test_rfc2617_response", + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_home_access", + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_item_access", + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_resource_access", + "eve/tests/auth.py::TestBasicAuth::test_unauthorized_schema_access", + "eve/tests/auth.py::TestTokenAuth::test_ALLOWED_ROLES_does_not_change", + "eve/tests/auth.py::TestTokenAuth::test_allowed_item_roles_does_not_change", + "eve/tests/auth.py::TestTokenAuth::test_allowed_roles_does_not_change", + "eve/tests/auth.py::TestTokenAuth::test_authorized_home_access", + "eve/tests/auth.py::TestTokenAuth::test_authorized_item_access", + "eve/tests/auth.py::TestTokenAuth::test_authorized_media_access", + "eve/tests/auth.py::TestTokenAuth::test_authorized_resource_access", + "eve/tests/auth.py::TestTokenAuth::test_authorized_schema_access", + "eve/tests/auth.py::TestTokenAuth::test_bad_auth_class", + "eve/tests/auth.py::TestTokenAuth::test_custom_auth", + "eve/tests/auth.py::TestTokenAuth::test_home_public_methods", + "eve/tests/auth.py::TestTokenAuth::test_instanced_auth", + "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_item", + "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_resource", + "eve/tests/auth.py::TestTokenAuth::test_public_methods_item", + "eve/tests/auth.py::TestTokenAuth::test_public_methods_resource", + "eve/tests/auth.py::TestTokenAuth::test_restricted_home_access", + "eve/tests/auth.py::TestTokenAuth::test_restricted_item_access", + "eve/tests/auth.py::TestTokenAuth::test_restricted_resource_access", + "eve/tests/auth.py::TestTokenAuth::test_rfc2617_response", + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_home_access", + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_item_access", + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_resource_access", + "eve/tests/auth.py::TestTokenAuth::test_unauthorized_schema_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_ALLOWED_ROLES_does_not_change", + "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_item_roles_does_not_change", + "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_roles_does_not_change", + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_home_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_item_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_media_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_resource_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_schema_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_bad_auth_class", + "eve/tests/auth.py::TestBearerTokenAuth::test_custom_auth", + "eve/tests/auth.py::TestBearerTokenAuth::test_home_public_methods", + "eve/tests/auth.py::TestBearerTokenAuth::test_instanced_auth", + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_item", + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_resource", + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_item", + "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_resource", + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_home_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_item_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_resource_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_rfc2617_response", + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_home_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_item_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_resource_access", + "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_schema_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_ALLOWED_ROLES_does_not_change", + "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_item_roles_does_not_change", + "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_roles_does_not_change", + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_home_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_item_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_media_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_resource_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_schema_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_bad_auth_class", + "eve/tests/auth.py::TestCustomTokenAuth::test_custom_auth", + "eve/tests/auth.py::TestCustomTokenAuth::test_home_public_methods", + "eve/tests/auth.py::TestCustomTokenAuth::test_instanced_auth", + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_item", + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_resource", + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_item", + "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_resource", + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_home_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_item_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_resource_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_rfc2617_response", + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_home_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_item_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_resource_access", + "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_schema_access", + "eve/tests/auth.py::TestHMACAuth::test_ALLOWED_ROLES_does_not_change", + "eve/tests/auth.py::TestHMACAuth::test_allowed_item_roles_does_not_change", + "eve/tests/auth.py::TestHMACAuth::test_allowed_roles_does_not_change", + "eve/tests/auth.py::TestHMACAuth::test_authorized_home_access", + "eve/tests/auth.py::TestHMACAuth::test_authorized_item_access", + "eve/tests/auth.py::TestHMACAuth::test_authorized_media_access", + "eve/tests/auth.py::TestHMACAuth::test_authorized_resource_access", + "eve/tests/auth.py::TestHMACAuth::test_authorized_schema_access", + "eve/tests/auth.py::TestHMACAuth::test_bad_auth_class", + "eve/tests/auth.py::TestHMACAuth::test_custom_auth", + "eve/tests/auth.py::TestHMACAuth::test_home_public_methods", + "eve/tests/auth.py::TestHMACAuth::test_instanced_auth", + "eve/tests/auth.py::TestHMACAuth::test_post_resource_hmac_auth", + "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_item", + "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_resource", + "eve/tests/auth.py::TestHMACAuth::test_public_methods_item", + "eve/tests/auth.py::TestHMACAuth::test_public_methods_resource", + "eve/tests/auth.py::TestHMACAuth::test_restricted_home_access", + "eve/tests/auth.py::TestHMACAuth::test_restricted_item_access", + "eve/tests/auth.py::TestHMACAuth::test_restricted_resource_access", + "eve/tests/auth.py::TestHMACAuth::test_rfc2617_response", + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_home_access", + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_item_access", + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_resource_access", + "eve/tests/auth.py::TestHMACAuth::test_unauthorized_schema_access", + "eve/tests/auth.py::TestResourceAuth::test_resource_only_auth", + "eve/tests/auth.py::TestUserRestrictedAccess::test_collection_get_public", + "eve/tests/auth.py::TestUserRestrictedAccess::test_delete", + "eve/tests/auth.py::TestUserRestrictedAccess::test_delete_item", + "eve/tests/auth.py::TestUserRestrictedAccess::test_filter_by_auth_field_id", + "eve/tests/auth.py::TestUserRestrictedAccess::test_get", + "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_criteria", + "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_id", + "eve/tests/auth.py::TestUserRestrictedAccess::test_item_get_public", + "eve/tests/auth.py::TestUserRestrictedAccess::test_patch", + "eve/tests/auth.py::TestUserRestrictedAccess::test_post", + "eve/tests/auth.py::TestUserRestrictedAccess::test_post_bandwidth_saver_off_resource_auth", + "eve/tests/auth.py::TestUserRestrictedAccess::test_post_resource_auth", + "eve/tests/auth.py::TestUserRestrictedAccess::test_put", + "eve/tests/auth.py::TestUserRestrictedAccess::test_put_bandwidth_saver_off_resource_auth", + "eve/tests/auth.py::TestUserRestrictedAccess::test_put_resource_auth", + "eve/tests/auth.py::TestUserRestrictedAccess::test_unique_to_user_on_post", + "eve/tests/config.py::TestConfig::test_allow_unknown_with_soft_delete", + "eve/tests/config.py::TestConfig::test_auth_field_as_custom_idfield", + "eve/tests/config.py::TestConfig::test_auth_field_as_idfield", + "eve/tests/config.py::TestConfig::test_create_indexes", + "eve/tests/config.py::TestConfig::test_custom_datalayer", + "eve/tests/config.py::TestConfig::test_custom_error_handlers", + "eve/tests/config.py::TestConfig::test_custom_import_name", + "eve/tests/config.py::TestConfig::test_custom_kwargs", + "eve/tests/config.py::TestConfig::test_custom_validator", + "eve/tests/config.py::TestConfig::test_datasource", + "eve/tests/config.py::TestConfig::test_default_datalayer", + "eve/tests/config.py::TestConfig::test_default_import_name", + "eve/tests/config.py::TestConfig::test_default_settings", + "eve/tests/config.py::TestConfig::test_default_validator", + "eve/tests/config.py::TestConfig::test_existing_env_config", + "eve/tests/config.py::TestConfig::test_mongodb_settings", + "eve/tests/config.py::TestConfig::test_oplog_config", + "eve/tests/config.py::TestConfig::test_pretty_resource_urls", + "eve/tests/config.py::TestConfig::test_regexconverter", + "eve/tests/config.py::TestConfig::test_register_resource", + "eve/tests/config.py::TestConfig::test_set_defaults", + "eve/tests/config.py::TestConfig::test_set_schema_defaults", + "eve/tests/config.py::TestConfig::test_settings_as_dict", + "eve/tests/config.py::TestConfig::test_unexisting_env_config", + "eve/tests/config.py::TestConfig::test_url_helpers", + "eve/tests/config.py::TestConfig::test_url_rules", + "eve/tests/config.py::TestConfig::test_validate_datecreated_in_schema", + "eve/tests/config.py::TestConfig::test_validate_domain_struct", + "eve/tests/config.py::TestConfig::test_validate_invalid_field_names", + "eve/tests/config.py::TestConfig::test_validate_item_methods", + "eve/tests/config.py::TestConfig::test_validate_lastupdated_in_schema", + "eve/tests/config.py::TestConfig::test_validate_resource_methods", + "eve/tests/config.py::TestConfig::test_validate_roles", + "eve/tests/config.py::TestConfig::test_validate_schema", + "eve/tests/config.py::TestConfig::test_validate_schema_item_methods", + "eve/tests/config.py::TestConfig::test_validate_schema_methods", + "eve/tests/endpoints.py::TestCustomConverters::test_delete_uuid", + "eve/tests/endpoints.py::TestCustomConverters::test_get_uuid", + "eve/tests/endpoints.py::TestCustomConverters::test_patch_uuid", + "eve/tests/endpoints.py::TestCustomConverters::test_post_uuid", + "eve/tests/endpoints.py::TestCustomConverters::test_put_uuid", + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix", + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_post_internal", + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version", + "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version_hateoas_links", + "eve/tests/endpoints.py::TestEndPoints::test_api_version", + "eve/tests/endpoints.py::TestEndPoints::test_homepage", + "eve/tests/endpoints.py::TestEndPoints::test_homepage_does_not_have_internal_resources", + "eve/tests/endpoints.py::TestEndPoints::test_internal_endpoint", + "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_additional_lookup", + "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_id", + "eve/tests/endpoints.py::TestEndPoints::test_item_self_link", + "eve/tests/endpoints.py::TestEndPoints::test_nested_endpoint", + "eve/tests/endpoints.py::TestEndPoints::test_oplog_endpoint", + "eve/tests/endpoints.py::TestEndPoints::test_resource_endpoint", + "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint", + "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint_does_not_attempt_callable_serialization", + "eve/tests/endpoints.py::TestEndPoints::test_unknown_endpoints", + "eve/tests/logging.py::TestUtils::test_logging_info", + "eve/tests/renders.py::TestRenders::test_CORS", + "eve/tests/renders.py::TestRenders::test_CORS_MAX_AGE", + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS", + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_item", + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_resources", + "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_schema", + "eve/tests/renders.py::TestRenders::test_CORS_regex", + "eve/tests/renders.py::TestRenders::test_default_render", + "eve/tests/renders.py::TestRenders::test_json_disabled", + "eve/tests/renders.py::TestRenders::test_json_keys_sorted", + "eve/tests/renders.py::TestRenders::test_json_render", + "eve/tests/renders.py::TestRenders::test_json_xml_disabled", + "eve/tests/renders.py::TestRenders::test_jsonp_enabled", + "eve/tests/renders.py::TestRenders::test_unknown_render", + "eve/tests/renders.py::TestRenders::test_xml_disabled", + "eve/tests/renders.py::TestRenders::test_xml_leaf_escaping", + "eve/tests/renders.py::TestRenders::test_xml_ordered_nodes", + "eve/tests/renders.py::TestRenders::test_xml_render", + "eve/tests/renders.py::TestRenders::test_xml_url_escaping", + "eve/tests/response.py::TestResponse::test_response_data", + "eve/tests/response.py::TestResponse::test_response_object", + "eve/tests/response.py::TestResponse::test_response_pretty", + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage", + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage_reply", + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_item", + "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_resource", + "eve/tests/response.py::TestNoHateoas::test_patch_no_hateoas", + "eve/tests/response.py::TestNoHateoas::test_post_no_hateoas", + "eve/tests/utils.py::TestUtils::test_date_to_str", + "eve/tests/utils.py::TestUtils::test_debug_error_message", + "eve/tests/utils.py::TestUtils::test_document_etag", + "eve/tests/utils.py::TestUtils::test_document_etag_ignore_fields", + "eve/tests/utils.py::TestUtils::test_extract_key_values", + "eve/tests/utils.py::TestUtils::test_import_from_string", + "eve/tests/utils.py::TestUtils::test_parse_request_if_match", + "eve/tests/utils.py::TestUtils::test_parse_request_if_modified_since", + "eve/tests/utils.py::TestUtils::test_parse_request_if_none_match", + "eve/tests/utils.py::TestUtils::test_parse_request_max_results", + "eve/tests/utils.py::TestUtils::test_parse_request_max_results_disabled_pagination", + "eve/tests/utils.py::TestUtils::test_parse_request_page", + "eve/tests/utils.py::TestUtils::test_parse_request_sort", + "eve/tests/utils.py::TestUtils::test_parse_request_where", + "eve/tests/utils.py::TestUtils::test_querydef", + "eve/tests/utils.py::TestUtils::test_str_to_date", + "eve/tests/utils.py::TestUtils::test_validate_filters", + "eve/tests/utils.py::TestUtils::test_weak_date", + "eve/tests/versioning.py::TestCompleteVersioning::test_automatic_fields", + "eve/tests/versioning.py::TestCompleteVersioning::test_delete", + "eve/tests/versioning.py::TestCompleteVersioning::test_deleteitem", + "eve/tests/versioning.py::TestCompleteVersioning::test_get", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_projection", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all_projection", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_bad_format", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_diffs", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_modified_since", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_none_match", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_pagination", + "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_unknown", + "eve/tests/versioning.py::TestCompleteVersioning::test_multi_post", + "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item", + "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item_contacts", + "eve/tests/versioning.py::TestCompleteVersioning::test_patch", + "eve/tests/versioning.py::TestCompleteVersioning::test_post", + "eve/tests/versioning.py::TestCompleteVersioning::test_put", + "eve/tests/versioning.py::TestCompleteVersioning::test_referential_integrity", + "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete", + "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete_version_db_fields", + "eve/tests/versioning.py::TestCompleteVersioning::test_version_control_the_unkown", + "eve/tests/versioning.py::TestVersionedDataRelation::test_embedded", + "eve/tests/versioning.py::TestVersionedDataRelation::test_referential_integrity", + "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_data_relation_validation", + "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_embedded", + "eve/tests/versioning.py::TestVersionedDataRelationCustomField::test_referential_integrity", + "eve/tests/versioning.py::TestVersionedDataRelationUnversionedField::test_referential_integrity", + "eve/tests/versioning.py::TestPartialVersioning::test_get", + "eve/tests/versioning.py::TestPartialVersioning::test_getitem", + "eve/tests/versioning.py::TestPartialVersioning::test_multi_post", + "eve/tests/versioning.py::TestPartialVersioning::test_patch", + "eve/tests/versioning.py::TestPartialVersioning::test_post", + "eve/tests/versioning.py::TestPartialVersioning::test_put", + "eve/tests/versioning.py::TestPartialVersioning::test_version_control_the_unkown", + "eve/tests/versioning.py::TestLateVersioning::test_datasource", + "eve/tests/versioning.py::TestLateVersioning::test_delete", + "eve/tests/versioning.py::TestLateVersioning::test_deleteitem", + "eve/tests/versioning.py::TestLateVersioning::test_embedded", + "eve/tests/versioning.py::TestLateVersioning::test_get", + "eve/tests/versioning.py::TestLateVersioning::test_getitem", + "eve/tests/versioning.py::TestLateVersioning::test_patch", + "eve/tests/versioning.py::TestLateVersioning::test_put", + "eve/tests/versioning.py::TestLateVersioning::test_referential_integrity", + "eve/tests/versioning.py::TestLateVersioning::test_softdelete", + "eve/tests/versioning.py::TestVersioningWithCustomIdField::test_getitem", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_config", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_mongo_url", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_auth_params_provided", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_options", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_port", + "eve/tests/io/flask_pymongo.py::TestPyMongo::test_valid_port", + "eve/tests/io/media.py::TestMediaStorage::test_base_media_storage", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_get_media_can_leverage_projection", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_base_url", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete_projection", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_errors", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch_null", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_excluded_file_in_result", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended_excluded_file_in_result", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_put", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_return_url", + "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_partial_media", + "eve/tests/io/mongo.py::TestPythonParser::test_And_BoolOp", + "eve/tests/io/mongo.py::TestPythonParser::test_Attribute", + "eve/tests/io/mongo.py::TestPythonParser::test_Eq", + "eve/tests/io/mongo.py::TestPythonParser::test_Gt", + "eve/tests/io/mongo.py::TestPythonParser::test_GtE", + "eve/tests/io/mongo.py::TestPythonParser::test_Lt", + "eve/tests/io/mongo.py::TestPythonParser::test_LtE", + "eve/tests/io/mongo.py::TestPythonParser::test_NotEq", + "eve/tests/io/mongo.py::TestPythonParser::test_ObjectId_Call", + "eve/tests/io/mongo.py::TestPythonParser::test_Or_BoolOp", + "eve/tests/io/mongo.py::TestPythonParser::test_bad_Expr", + "eve/tests/io/mongo.py::TestPythonParser::test_datetime_Call", + "eve/tests/io/mongo.py::TestPythonParser::test_nested_BoolOp", + "eve/tests/io/mongo.py::TestPythonParser::test_unparsed_statement", + "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_dependencies_with_defaults", + "eve/tests/io/mongo.py::TestMongoValidator::test_feature_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_feature_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_geojson_not_compilant", + "eve/tests/io/mongo.py::TestMongoValidator::test_geometry_not_compilant", + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_not_compilant", + "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_multilinestring_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_multipoint_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_multipolygon_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_point_coordinates_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_point_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_point_integer_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_point_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_success", + "eve/tests/io/mongo.py::TestMongoValidator::test_reject_invalid_schema", + "eve/tests/io/mongo.py::TestMongoValidator::test_unique_fail", + "eve/tests/io/mongo.py::TestMongoValidator::test_unique_success", + "eve/tests/io/mongo.py::TestMongoDriver::test_combine_queries", + "eve/tests/io/mongo.py::TestMongoDriver::test_delete_returns_status", + "eve/tests/io/mongo.py::TestMongoDriver::test_get_value_from_query", + "eve/tests/io/mongo.py::TestMongoDriver::test_json_encoder_class", + "eve/tests/io/mongo.py::TestMongoDriver::test_query_contains_field", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_create_index_with_mongo_uri_and_prefix", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_delete_multidb", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_get_multidb", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_patch_multidb", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_post_multidb", + "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_put_multidb", + "eve/tests/io/multi_mongo.py::TestMultiMongoAuth::test_get_multidb", + "eve/tests/methods/common.py::TestSerializer::test_dbref_serialize_lists_of_lists", + "eve/tests/methods/common.py::TestSerializer::test_mongo_serializes", + "eve/tests/methods/common.py::TestSerializer::test_non_blocking_on_simple_field_serialization_exception", + "eve/tests/methods/common.py::TestSerializer::test_serialize_alongside_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_boolean", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_schema_of_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_typesavers", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_nested_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_typesavers", + "eve/tests/methods/common.py::TestSerializer::test_serialize_list_alongside_x_of_rules", + "eve/tests/methods/common.py::TestSerializer::test_serialize_lists_of_lists", + "eve/tests/methods/common.py::TestSerializer::test_serialize_null_dictionary", + "eve/tests/methods/common.py::TestSerializer::test_serialize_null_list", + "eve/tests/methods/common.py::TestSerializer::test_serialize_number", + "eve/tests/methods/common.py::TestSerializer::test_serialize_subdocument", + "eve/tests/methods/common.py::TestNormalizeDottedFields::test_normalize_dotted_fields", + "eve/tests/methods/common.py::TestOpLogEndpointDisabled::test_post_oplog", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_delete_oplog", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_oplog_hook", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_patch_oplog", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_basic_auth", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_hmac_auth", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_token_auth", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog_does_not_alter_document", + "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_soft_delete_oplog", + "eve/tests/methods/common.py::TestTickets::test_ticket_681", + "eve/tests/methods/delete.py::TestDelete::test_bulk_delete_id_field", + "eve/tests/methods/delete.py::TestDelete::test_delete", + "eve/tests/methods/delete.py::TestDelete::test_delete_custom_idfield", + "eve/tests/methods/delete.py::TestDelete::test_delete_different_resource", + "eve/tests/methods/delete.py::TestDelete::test_delete_empty_resource", + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint", + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_different_resource", + "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_write_concern", + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_bad_etag", + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_disabled", + "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_missing", + "eve/tests/methods/delete.py::TestDelete::test_delete_non_existant", + "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource", + "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource_with_override", + "eve/tests/methods/delete.py::TestDelete::test_delete_subresource", + "eve/tests/methods/delete.py::TestDelete::test_delete_subresource_item", + "eve/tests/methods/delete.py::TestDelete::test_delete_unknown_item", + "eve/tests/methods/delete.py::TestDelete::test_delete_with_post_override", + "eve/tests/methods/delete.py::TestDelete::test_delete_write_concern", + "eve/tests/methods/delete.py::TestDelete::test_deleteitem_internal", + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_disabled_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestDelete::test_ifmatch_missing_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestDelete::test_unknown_resource", + "eve/tests/methods/delete.py::TestSoftDelete::test_bulk_delete_id_field", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_custom_idfield", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_different_resource", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_empty_resource", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_different_resource", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_write_concern", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_bad_etag", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_disabled", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_missing", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_non_existant", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource_with_override", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource_item", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_unknown_item", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_with_post_override", + "eve/tests/methods/delete.py::TestSoftDelete::test_delete_write_concern", + "eve/tests/methods/delete.py::TestSoftDelete::test_deleteitem_internal", + "eve/tests/methods/delete.py::TestSoftDelete::test_exclude_soft_deleted_documents_from_unique_checks", + "eve/tests/methods/delete.py::TestSoftDelete::test_exclusive_projection", + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_disabled_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_missing_enforce_ifmatch_disabled", + "eve/tests/methods/delete.py::TestSoftDelete::test_multiple_softdelete", + "eve/tests/methods/delete.py::TestSoftDelete::test_restore_softdeleted", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_caching", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_datalayer", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_db_fields", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_deleted_field", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_show_deleted", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_embedded_doc", + "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_get_response_skips_embedded_expansion", + "eve/tests/methods/delete.py::TestSoftDelete::test_unknown_resource", + "eve/tests/methods/delete.py::TestResourceSpecificSoftDelete::test_resource_specific_softdelete", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item_contacts", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource_contacts", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item_contacts", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_resource_contacts", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_resource", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_resource", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_dynamic_filter", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_resource", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_item", + "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_resource", + "eve/tests/methods/get.py::TestGet::test_cache_control", + "eve/tests/methods/get.py::TestGet::test_cursor_extra_find", + "eve/tests/methods/get.py::TestGet::test_documents_missing_standard_date_fields", + "eve/tests/methods/get.py::TestGet::test_expires", + "eve/tests/methods/get.py::TestGet::test_get", + "eve/tests/methods/get.py::TestGet::test_get_aggregation_endpoint", + "eve/tests/methods/get.py::TestGet::test_get_aggregation_pagination", + "eve/tests/methods/get.py::TestGet::test_get_aggregation_parsing", + "eve/tests/methods/get.py::TestGet::test_get_aggregation_with_lists", + "eve/tests/methods/get.py::TestGet::test_get_allowed_filters_operators", + "eve/tests/methods/get.py::TestGet::test_get_custom_auto_document_fields", + "eve/tests/methods/get.py::TestGet::test_get_custom_embedded", + "eve/tests/methods/get.py::TestGet::test_get_custom_hateoas_links", + "eve/tests/methods/get.py::TestGet::test_get_custom_idfield", + "eve/tests/methods/get.py::TestGet::test_get_custom_items", + "eve/tests/methods/get.py::TestGet::test_get_custom_links", + "eve/tests/methods/get.py::TestGet::test_get_custom_max_results", + "eve/tests/methods/get.py::TestGet::test_get_custom_page", + "eve/tests/methods/get.py::TestGet::test_get_custom_params", + "eve/tests/methods/get.py::TestGet::test_get_custom_projection", + "eve/tests/methods/get.py::TestGet::test_get_custom_sort", + "eve/tests/methods/get.py::TestGet::test_get_custom_where", + "eve/tests/methods/get.py::TestGet::test_get_default_sort", + "eve/tests/methods/get.py::TestGet::test_get_embedded", + "eve/tests/methods/get.py::TestGet::test_get_embedded_media", + "eve/tests/methods/get.py::TestGet::test_get_embedded_media_validate_rest_of_fields", + "eve/tests/methods/get.py::TestGet::test_get_empty_resource", + "eve/tests/methods/get.py::TestGet::test_get_idfield_doesnt_exist", + "eve/tests/methods/get.py::TestGet::test_get_ifmatch_disabled", + "eve/tests/methods/get.py::TestGet::test_get_ims_empty_resource", + "eve/tests/methods/get.py::TestGet::test_get_internal_page", + "eve/tests/methods/get.py::TestGet::test_get_invalid_idfield_cors", + "eve/tests/methods/get.py::TestGet::test_get_invalid_sort_syntax", + "eve/tests/methods/get.py::TestGet::test_get_invalid_where_fields", + "eve/tests/methods/get.py::TestGet::test_get_invalid_where_syntax", + "eve/tests/methods/get.py::TestGet::test_get_lookup_field_as_string", + "eve/tests/methods/get.py::TestGet::test_get_max_results", + "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist", + "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist_nested", + "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_unvalidated", + "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_validated", + "eve/tests/methods/get.py::TestGet::test_get_nested_resource", + "eve/tests/methods/get.py::TestGet::test_get_page", + "eve/tests/methods/get.py::TestGet::test_get_pagination_no_documents", + "eve/tests/methods/get.py::TestGet::test_get_paging_disabled_no_args", + "eve/tests/methods/get.py::TestGet::test_get_perform_count_on_pagination_disabled", + "eve/tests/methods/get.py::TestGet::test_get_projection", + "eve/tests/methods/get.py::TestGet::test_get_projection_consistent_etag", + "eve/tests/methods/get.py::TestGet::test_get_projection_noschema", + "eve/tests/methods/get.py::TestGet::test_get_projection_subdocument", + "eve/tests/methods/get.py::TestGet::test_get_query_bitwise_query_operators", + "eve/tests/methods/get.py::TestGet::test_get_query_in_links", + "eve/tests/methods/get.py::TestGet::test_get_reference_embedded_in_subdocuments", + "eve/tests/methods/get.py::TestGet::test_get_resource_title", + "eve/tests/methods/get.py::TestGet::test_get_same_collection_different_resource", + "eve/tests/methods/get.py::TestGet::test_get_server_exclude_projection_can_project_others", + "eve/tests/methods/get.py::TestGet::test_get_server_exlcude_projection_can_sniff", + "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_block_sniff", + "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_can_exclude", + "eve/tests/methods/get.py::TestGet::test_get_sort_comma_delimited_syntax", + "eve/tests/methods/get.py::TestGet::test_get_sort_disabled", + "eve/tests/methods/get.py::TestGet::test_get_sort_mongo_syntax", + "eve/tests/methods/get.py::TestGet::test_get_static_projection", + "eve/tests/methods/get.py::TestGet::test_get_subresource", + "eve/tests/methods/get.py::TestGet::test_get_subresource_with_custom_idfield", + "eve/tests/methods/get.py::TestGet::test_get_total_count_header", + "eve/tests/methods/get.py::TestGet::test_get_where_allowed_filters", + "eve/tests/methods/get.py::TestGet::test_get_where_disabled", + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_combined_date", + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_objectid_as_string", + "eve/tests/methods/get.py::TestGet::test_get_where_mongo_syntax", + "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax", + "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax1", + "eve/tests/methods/get.py::TestGet::test_get_with_post_override", + "eve/tests/methods/get.py::TestGetItem::test_cache_control", + "eve/tests/methods/get.py::TestGetItem::test_disallowed_getitem", + "eve/tests/methods/get.py::TestGetItem::test_expires", + "eve/tests/methods/get.py::TestGetItem::test_get_with_post_override", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id_different_resource", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_integer", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_different_resource", + "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_self_href", + "eve/tests/methods/get.py::TestGetItem::test_getitem_custom_auto_document_fields", + "eve/tests/methods/get.py::TestGetItem::test_getitem_embedded", + "eve/tests/methods/get.py::TestGetItem::test_getitem_if_modified_since", + "eve/tests/methods/get.py::TestGetItem::test_getitem_if_none_match", + "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled", + "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled_if_mod_since", + "eve/tests/methods/get.py::TestGetItem::test_getitem_internal_by_id", + "eve/tests/methods/get.py::TestGetItem::test_getitem_lookup_field_as_string", + "eve/tests/methods/get.py::TestGetItem::test_getitem_missing_standard_date_fields", + "eve/tests/methods/get.py::TestGetItem::test_getitem_noschema", + "eve/tests/methods/get.py::TestGetItem::test_getitem_projection", + "eve/tests/methods/get.py::TestGetItem::test_getitem_with_custom_idfield", + "eve/tests/methods/get.py::TestGetItem::test_subresource_getitem", + "eve/tests/methods/get.py::TestHead::test_head_home", + "eve/tests/methods/get.py::TestHead::test_head_item", + "eve/tests/methods/get.py::TestHead::test_head_resource", + "eve/tests/methods/get.py::TestEvents::test_get_after_aggregation_hook", + "eve/tests/methods/get.py::TestEvents::test_get_before_aggregation_hook", + "eve/tests/methods/get.py::TestEvents::test_on_fetched_item", + "eve/tests/methods/get.py::TestEvents::test_on_fetched_item_contacts", + "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource", + "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource_contacts", + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_item", + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_resource", + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_homepage", + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_item", + "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_resource", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_item", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_resource", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_item_dynamic_filter", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter_12_chr_nonunicode_string", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_item", + "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_resource", + "eve/tests/methods/patch.py::TestPatch::test_by_name", + "eve/tests/methods/patch.py::TestPatch::test_id_field_in_document_fails", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag_enforce_ifmatch_disabled", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled_enforce_ifmatch_disabled", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing", + "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing_enforce_ifmatch_disabled", + "eve/tests/methods/patch.py::TestPatch::test_patch_allow_unknown", + "eve/tests/methods/patch.py::TestPatch::test_patch_bandwidth_saver", + "eve/tests/methods/patch.py::TestPatch::test_patch_custom_idfield", + "eve/tests/methods/patch.py::TestPatch::test_patch_datetime", + "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_on_origin_document", + "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_value_on_origin_document", + "eve/tests/methods/patch.py::TestPatch::test_patch_dict", + "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header", + "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header_enforce_ifmatch_disabled", + "eve/tests/methods/patch.py::TestPatch::test_patch_integer", + "eve/tests/methods/patch.py::TestPatch::test_patch_internal", + "eve/tests/methods/patch.py::TestPatch::test_patch_list", + "eve/tests/methods/patch.py::TestPatch::test_patch_list_as_array", + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default", + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default_with_post_override", + "eve/tests/methods/patch.py::TestPatch::test_patch_missing_standard_date_fields", + "eve/tests/methods/patch.py::TestPatch::test_patch_multiple_fields", + "eve/tests/methods/patch.py::TestPatch::test_patch_nested", + "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_not_overwritten", + "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_nullable_missing", + "eve/tests/methods/patch.py::TestPatch::test_patch_null_objectid", + "eve/tests/methods/patch.py::TestPatch::test_patch_objectid", + "eve/tests/methods/patch.py::TestPatch::test_patch_readonly_field_with_previous_document", + "eve/tests/methods/patch.py::TestPatch::test_patch_referential_integrity", + "eve/tests/methods/patch.py::TestPatch::test_patch_rows", + "eve/tests/methods/patch.py::TestPatch::test_patch_string", + "eve/tests/methods/patch.py::TestPatch::test_patch_subresource", + "eve/tests/methods/patch.py::TestPatch::test_patch_to_resource_endpoint", + "eve/tests/methods/patch.py::TestPatch::test_patch_type_coercion", + "eve/tests/methods/patch.py::TestPatch::test_patch_with_post_override", + "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_fail", + "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_success", + "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded", + "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded_number_serialization", + "eve/tests/methods/patch.py::TestPatch::test_readonly_resource", + "eve/tests/methods/patch.py::TestPatch::test_unique_value", + "eve/tests/methods/patch.py::TestPatch::test_unknown_id", + "eve/tests/methods/patch.py::TestPatch::test_unknown_id_different_resource", + "eve/tests/methods/patch.py::TestEvents::test_on_PATCH_dynamic_filter", + "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH", + "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH_contacts", + "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH", + "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH_contacts", + "eve/tests/methods/patch.py::TestEvents::test_on_update", + "eve/tests/methods/patch.py::TestEvents::test_on_update_contacts", + "eve/tests/methods/patch.py::TestEvents::test_on_updated", + "eve/tests/methods/patch.py::TestEvents::test_on_updated_contacts", + "eve/tests/methods/post.py::TestPost::test_custom_date_updated", + "eve/tests/methods/post.py::TestPost::test_custom_etag_update_date", + "eve/tests/methods/post.py::TestPost::test_custom_issues", + "eve/tests/methods/post.py::TestPost::test_custom_status", + "eve/tests/methods/post.py::TestPost::test_dbref_post_referential_integrity", + "eve/tests/methods/post.py::TestPost::test_id_field_included_with_document", + "eve/tests/methods/post.py::TestPost::test_multi_post_invalid", + "eve/tests/methods/post.py::TestPost::test_multi_post_valid", + "eve/tests/methods/post.py::TestPost::test_post_allow_unknown", + "eve/tests/methods/post.py::TestPost::test_post_alternative_payload", + "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_media_list", + "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_multiple_keys", + "eve/tests/methods/post.py::TestPost::test_post_auto_create_lists", + "eve/tests/methods/post.py::TestPost::test_post_bandwidth_saver", + "eve/tests/methods/post.py::TestPost::test_post_bulk_insert_on_disabled_bulk", + "eve/tests/methods/post.py::TestPost::test_post_custom_idfield", + "eve/tests/methods/post.py::TestPost::test_post_custom_json_content_type", + "eve/tests/methods/post.py::TestPost::test_post_datetime", + "eve/tests/methods/post.py::TestPost::test_post_decimal_number_fail", + "eve/tests/methods/post.py::TestPost::test_post_decimal_number_success", + "eve/tests/methods/post.py::TestPost::test_post_default_value", + "eve/tests/methods/post.py::TestPost::test_post_default_value_none", + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_default", + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_subdocuments", + "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_values", + "eve/tests/methods/post.py::TestPost::test_post_dependency_required_fields", + "eve/tests/methods/post.py::TestPost::test_post_dict", + "eve/tests/methods/post.py::TestPost::test_post_duplicate_key", + "eve/tests/methods/post.py::TestPost::test_post_empty_bulk_insert", + "eve/tests/methods/post.py::TestPost::test_post_empty_resource", + "eve/tests/methods/post.py::TestPost::test_post_error_as_list", + "eve/tests/methods/post.py::TestPost::test_post_float_zero", + "eve/tests/methods/post.py::TestPost::test_post_ifmatch_disabled", + "eve/tests/methods/post.py::TestPost::test_post_integer", + "eve/tests/methods/post.py::TestPost::test_post_integer_zero", + "eve/tests/methods/post.py::TestPost::test_post_internal", + "eve/tests/methods/post.py::TestPost::test_post_internal_skip_validation", + "eve/tests/methods/post.py::TestPost::test_post_keyschema_dict", + "eve/tests/methods/post.py::TestPost::test_post_list", + "eve/tests/methods/post.py::TestPost::test_post_list_as_array", + "eve/tests/methods/post.py::TestPost::test_post_list_fixed_len", + "eve/tests/methods/post.py::TestPost::test_post_list_of_objectid", + "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_off", + "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_on", + "eve/tests/methods/post.py::TestPost::test_post_nested", + "eve/tests/methods/post.py::TestPost::test_post_nested_dict_objectid", + "eve/tests/methods/post.py::TestPost::test_post_null_objectid", + "eve/tests/methods/post.py::TestPost::test_post_objectid", + "eve/tests/methods/post.py::TestPost::test_post_readonly_field_with_default", + "eve/tests/methods/post.py::TestPost::test_post_readonly_in_dict", + "eve/tests/methods/post.py::TestPost::test_post_referential_integrity", + "eve/tests/methods/post.py::TestPost::test_post_referential_integrity_list", + "eve/tests/methods/post.py::TestPost::test_post_rows", + "eve/tests/methods/post.py::TestPost::test_post_string", + "eve/tests/methods/post.py::TestPost::test_post_to_item_endpoint", + "eve/tests/methods/post.py::TestPost::test_post_type_coercion", + "eve/tests/methods/post.py::TestPost::test_post_valueschema_dict", + "eve/tests/methods/post.py::TestPost::test_post_valueschema_with_objectid", + "eve/tests/methods/post.py::TestPost::test_post_with_content_type_charset", + "eve/tests/methods/post.py::TestPost::test_post_with_excluded_response_fields", + "eve/tests/methods/post.py::TestPost::test_post_with_extra_response_fields", + "eve/tests/methods/post.py::TestPost::test_post_with_get_override", + "eve/tests/methods/post.py::TestPost::test_post_with_relation_to_custom_idfield", + "eve/tests/methods/post.py::TestPost::test_post_write_concern", + "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded", + "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded_number_serialization", + "eve/tests/methods/post.py::TestPost::test_readonly_resource", + "eve/tests/methods/post.py::TestPost::test_subresource", + "eve/tests/methods/post.py::TestPost::test_subresource_required_ref", + "eve/tests/methods/post.py::TestPost::test_unknown_resource", + "eve/tests/methods/post.py::TestPost::test_validation_error", + "eve/tests/methods/post.py::TestEvents::test_on_POST_post_resource", + "eve/tests/methods/post.py::TestEvents::test_on_insert", + "eve/tests/methods/post.py::TestEvents::test_on_insert_contacts", + "eve/tests/methods/post.py::TestEvents::test_on_inserted", + "eve/tests/methods/post.py::TestEvents::test_on_inserted_contacts", + "eve/tests/methods/post.py::TestEvents::test_on_post_POST", + "eve/tests/methods/post.py::TestEvents::test_on_pre_POST", + "eve/tests/methods/post.py::TestEvents::test_on_pre_POST_contacts", + "eve/tests/methods/put.py::TestPut::test_allow_unknown", + "eve/tests/methods/put.py::TestPut::test_by_name", + "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag", + "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag_enforce_ifmatch_disabled", + "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled", + "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled_enforce_ifmatch_disabled", + "eve/tests/methods/put.py::TestPut::test_ifmatch_missing", + "eve/tests/methods/put.py::TestPut::test_ifmatch_missing_enforce_ifmatch_disabled", + "eve/tests/methods/put.py::TestPut::test_put_bandwidth_saver", + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document", + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_fails_on_mismatching_id", + "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_with_url_as_id", + "eve/tests/methods/put.py::TestPut::test_put_custom_idfield", + "eve/tests/methods/put.py::TestPut::test_put_dbref_subresource", + "eve/tests/methods/put.py::TestPut::test_put_default_value", + "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_default", + "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_wrong_value", + "eve/tests/methods/put.py::TestPut::test_put_etag_header", + "eve/tests/methods/put.py::TestPut::test_put_etag_header_enforce_ifmatch_disabled", + "eve/tests/methods/put.py::TestPut::test_put_internal", + "eve/tests/methods/put.py::TestPut::test_put_internal_skip_validation", + "eve/tests/methods/put.py::TestPut::test_put_nested", + "eve/tests/methods/put.py::TestPut::test_put_readonly_value_different", + "eve/tests/methods/put.py::TestPut::test_put_readonly_value_same", + "eve/tests/methods/put.py::TestPut::test_put_referential_integrity", + "eve/tests/methods/put.py::TestPut::test_put_referential_integrity_list", + "eve/tests/methods/put.py::TestPut::test_put_returns_404_on_unexisting_document", + "eve/tests/methods/put.py::TestPut::test_put_string", + "eve/tests/methods/put.py::TestPut::test_put_subresource", + "eve/tests/methods/put.py::TestPut::test_put_to_resource_endpoint", + "eve/tests/methods/put.py::TestPut::test_put_type_coercion", + "eve/tests/methods/put.py::TestPut::test_put_with_post_override", + "eve/tests/methods/put.py::TestPut::test_put_write_concern_fail", + "eve/tests/methods/put.py::TestPut::test_put_write_concern_success", + "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded", + "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded_number_serialization", + "eve/tests/methods/put.py::TestPut::test_readonly_resource", + "eve/tests/methods/put.py::TestPut::test_unique_value", + "eve/tests/methods/put.py::TestEvents::test_on_post_PUT", + "eve/tests/methods/put.py::TestEvents::test_on_post_PUT_contacts", + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT", + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_contacts", + "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_dynamic_filter", + "eve/tests/methods/put.py::TestEvents::test_on_replace", + "eve/tests/methods/put.py::TestEvents::test_on_replace_contacts", + "eve/tests/methods/put.py::TestEvents::test_on_replaced", + "eve/tests/methods/put.py::TestEvents::test_on_replaced_contacts", + "eve/tests/methods/ratelimit.py::TestRateLimit::test_noratelimits", + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_home", + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_item", + "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_resource" +] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index b49810930..f9580ab65 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,7 @@ Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.6-3.6, and PyPy. +The codebase is thoroughly tested under Python 2.7, 3.4+, and PyPy. Eve is Simple ------------- diff --git a/docs/testing.rst b/docs/testing.rst index 987cf565a..92800649a 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,7 +1,7 @@ Running the Tests ================= -Eve runs under Python 2.6, Python 2.7, Python 3.3 and PyPy. Therefore tests -will be run in those four platforms in our `continuous integration server`_. +Eve runs under Python 2.7, 3.4+, and PyPy. Therefore tests will be run in those +four platforms in our `continuous integration server`_. The easiest way to get started is to run the tests in your local environment with: @@ -48,8 +48,8 @@ by running :: Testing with other python versions ---------------------------------- Before you submit a pull request, make sure your tests and changes run in -all supported python versions: 2.6, 2.7, 3.3, 3.4, 3.5 and PyPy. Instead of creating all -those environments by hand, Eve uses tox_. +all supported python versions: 2.7, 3.4, 3.5, 3.6, and PyPy. Instead of +creating all those environments by hand, Eve uses tox_. Make sure you have all required python versions installed and run: @@ -65,11 +65,10 @@ the following: .. code-block:: console _________ summary _________ - py26: commands succeeded py27: commands succeeded - py33: commands succeeded py34: commands succeeded py35: commands succeeded + py36: commands succeeded pypy: commands succeeded flake8: commands succeeded congratulations :) From 58420da8127023ad4ea88633c2e3e307ba9ad1c6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 27 Apr 2018 17:50:40 +0200 Subject: [PATCH 312/821] Flask 1.0+ is now required --- CHANGES | 2 +- eve/tests/logging.py | 2 +- requirements.txt | 9 --------- setup.py | 8 ++------ 4 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 requirements.txt diff --git a/CHANGES b/CHANGES index deeb53b51..bcee810be 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Development Version 0.8 ~~~~~~~~~~~ +- Flask requirement set to >=1.0. Closes #1111. - Python 2.6 and Python 3.3 are no longer supported. - Tests: finally acknowledge the existence of modern APIs for both Mongo and Python (get rid of most deprecation warnings). @@ -34,7 +35,6 @@ Version 0.8 sub-document fields. Closes #1123 (Luca Moretto). - Fix documentation typos (Olof Johansson) - Fix a changelog typo (kreynen). -- Update: bump Flask requirement to <1.0. Closes #1111. - Fix: broken documentation links to Cerberus validation rules. - New: Renderer classes. ``RENDERER`` allows to change enabled renderers. Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You diff --git a/eve/tests/logging.py b/eve/tests/logging.py index 9d86bdd86..8a6f21fac 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -13,7 +13,7 @@ def test_logging_info(self, l): self.app.logger.propagate = True self.app.logger.info('test info') l.check( - ('eve', 'INFO', 'test info') + ('flask.app', 'INFO', 'test info') ) log_record = l.records[0] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d2613b7f2..000000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Cerberus==1.1 -Events==0.3 -Flask==0.12.3 -itsdangerous==0.24 -Jinja2==2.10 -MarkupSafe==0.23 -pymongo==3.5.0 -simplejson==3.8.2 -Werkzeug==0.14.1 diff --git a/setup.py b/setup.py index 67a133b4e..84620e04c 100755 --- a/setup.py +++ b/setup.py @@ -11,13 +11,9 @@ install_requires = [ 'cerberus>=1.1', 'events>=0.3,<0.4', - 'simplejson>=3.3.0,<4.0', - 'werkzeug>=0.9.4,<=0.14', - 'markupsafe>=0.23,<1.0', - 'jinja2>=2.8,<3.0', - 'itsdangerous>=0.24,<1.0', - 'flask>=0.10.1,<1.0', + 'flask>=1.0', 'pymongo>=3.5', + 'simplejson>=3.3.0,<4.0', ] setup( From 77f28d792dfee4da830a2666306551695a3edee2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 7 May 2018 09:22:25 +0200 Subject: [PATCH 313/821] Add .pytest_cache to ignored folders --- .gitignore | 1 + .pytest_cache/v/cache/lastfailed | 789 ------------------------------- .pytest_cache/v/cache/nodeids | 779 ------------------------------ 3 files changed, 1 insertion(+), 1568 deletions(-) delete mode 100644 .pytest_cache/v/cache/lastfailed delete mode 100644 .pytest_cache/v/cache/nodeids diff --git a/.gitignore b/.gitignore index 76838eaee..b3a3ac866 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ _build .cache .vscode +.pytest_cache diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed deleted file mode 100644 index e1520469e..000000000 --- a/.pytest_cache/v/cache/lastfailed +++ /dev/null @@ -1,789 +0,0 @@ -{ - "eve/tests/auth.py::TestBasicAuth::test_restricted_item_access": true, - "eve/tests/auth.py::TestBasicAuth::test_restricted_resource_access": true, - "eve/tests/auth.py::TestBasicAuth::test_rfc2617_response": true, - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_home_access": true, - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_item_access": true, - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_resource_access": true, - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_schema_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_ALLOWED_ROLES_does_not_change": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_item_roles_does_not_change": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_roles_does_not_change": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_home_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_item_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_media_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_resource_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_schema_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_bad_auth_class": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_custom_auth": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_home_public_methods": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_instanced_auth": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_item": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_resource": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_item": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_resource": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_home_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_item_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_resource_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_rfc2617_response": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_home_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_item_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_resource_access": true, - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_schema_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_ALLOWED_ROLES_does_not_change": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_item_roles_does_not_change": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_roles_does_not_change": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_home_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_item_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_media_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_resource_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_schema_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_bad_auth_class": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_custom_auth": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_home_public_methods": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_instanced_auth": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_item": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_resource": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_item": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_resource": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_home_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_item_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_resource_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_rfc2617_response": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_home_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_item_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_resource_access": true, - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_schema_access": true, - "eve/tests/auth.py::TestHMACAuth::test_ALLOWED_ROLES_does_not_change": true, - "eve/tests/auth.py::TestHMACAuth::test_allowed_item_roles_does_not_change": true, - "eve/tests/auth.py::TestHMACAuth::test_allowed_roles_does_not_change": true, - "eve/tests/auth.py::TestHMACAuth::test_authorized_home_access": true, - "eve/tests/auth.py::TestHMACAuth::test_authorized_item_access": true, - "eve/tests/auth.py::TestHMACAuth::test_authorized_media_access": true, - "eve/tests/auth.py::TestHMACAuth::test_authorized_resource_access": true, - "eve/tests/auth.py::TestHMACAuth::test_authorized_schema_access": true, - "eve/tests/auth.py::TestHMACAuth::test_bad_auth_class": true, - "eve/tests/auth.py::TestHMACAuth::test_custom_auth": true, - "eve/tests/auth.py::TestHMACAuth::test_home_public_methods": true, - "eve/tests/auth.py::TestHMACAuth::test_instanced_auth": true, - "eve/tests/auth.py::TestHMACAuth::test_post_resource_hmac_auth": true, - "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_item": true, - "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_resource": true, - "eve/tests/auth.py::TestHMACAuth::test_public_methods_item": true, - "eve/tests/auth.py::TestHMACAuth::test_public_methods_resource": true, - "eve/tests/auth.py::TestHMACAuth::test_restricted_home_access": true, - "eve/tests/auth.py::TestHMACAuth::test_restricted_item_access": true, - "eve/tests/auth.py::TestHMACAuth::test_restricted_resource_access": true, - "eve/tests/auth.py::TestHMACAuth::test_rfc2617_response": true, - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_home_access": true, - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_item_access": true, - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_resource_access": true, - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_schema_access": true, - "eve/tests/auth.py::TestResourceAuth::test_resource_only_auth": true, - "eve/tests/auth.py::TestTokenAuth::test_ALLOWED_ROLES_does_not_change": true, - "eve/tests/auth.py::TestTokenAuth::test_allowed_item_roles_does_not_change": true, - "eve/tests/auth.py::TestTokenAuth::test_allowed_roles_does_not_change": true, - "eve/tests/auth.py::TestTokenAuth::test_authorized_home_access": true, - "eve/tests/auth.py::TestTokenAuth::test_authorized_item_access": true, - "eve/tests/auth.py::TestTokenAuth::test_authorized_media_access": true, - "eve/tests/auth.py::TestTokenAuth::test_authorized_resource_access": true, - "eve/tests/auth.py::TestTokenAuth::test_authorized_schema_access": true, - "eve/tests/auth.py::TestTokenAuth::test_bad_auth_class": true, - "eve/tests/auth.py::TestTokenAuth::test_custom_auth": true, - "eve/tests/auth.py::TestTokenAuth::test_home_public_methods": true, - "eve/tests/auth.py::TestTokenAuth::test_instanced_auth": true, - "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_item": true, - "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_resource": true, - "eve/tests/auth.py::TestTokenAuth::test_public_methods_item": true, - "eve/tests/auth.py::TestTokenAuth::test_public_methods_resource": true, - "eve/tests/auth.py::TestTokenAuth::test_restricted_home_access": true, - "eve/tests/auth.py::TestTokenAuth::test_restricted_item_access": true, - "eve/tests/auth.py::TestTokenAuth::test_restricted_resource_access": true, - "eve/tests/auth.py::TestTokenAuth::test_rfc2617_response": true, - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_home_access": true, - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_item_access": true, - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_resource_access": true, - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_schema_access": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_collection_get_public": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_delete": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_delete_item": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_filter_by_auth_field_id": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_get": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_criteria": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_id": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_item_get_public": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_patch": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_post": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_post_bandwidth_saver_off_resource_auth": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_post_resource_auth": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_put": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_put_bandwidth_saver_off_resource_auth": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_put_resource_auth": true, - "eve/tests/auth.py::TestUserRestrictedAccess::test_unique_to_user_on_post": true, - "eve/tests/config.py::TestConfig::test_allow_unknown_with_soft_delete": true, - "eve/tests/config.py::TestConfig::test_auth_field_as_custom_idfield": true, - "eve/tests/config.py::TestConfig::test_auth_field_as_idfield": true, - "eve/tests/config.py::TestConfig::test_create_indexes": true, - "eve/tests/config.py::TestConfig::test_custom_datalayer": true, - "eve/tests/config.py::TestConfig::test_custom_error_handlers": true, - "eve/tests/config.py::TestConfig::test_custom_import_name": true, - "eve/tests/config.py::TestConfig::test_custom_kwargs": true, - "eve/tests/config.py::TestConfig::test_custom_validator": true, - "eve/tests/config.py::TestConfig::test_datasource": true, - "eve/tests/config.py::TestConfig::test_default_datalayer": true, - "eve/tests/config.py::TestConfig::test_default_import_name": true, - "eve/tests/config.py::TestConfig::test_default_settings": true, - "eve/tests/config.py::TestConfig::test_default_validator": true, - "eve/tests/config.py::TestConfig::test_existing_env_config": true, - "eve/tests/config.py::TestConfig::test_mongodb_settings": true, - "eve/tests/config.py::TestConfig::test_oplog_config": true, - "eve/tests/config.py::TestConfig::test_pretty_resource_urls": true, - "eve/tests/config.py::TestConfig::test_regexconverter": true, - "eve/tests/config.py::TestConfig::test_register_resource": true, - "eve/tests/config.py::TestConfig::test_set_defaults": true, - "eve/tests/config.py::TestConfig::test_set_schema_defaults": true, - "eve/tests/config.py::TestConfig::test_settings_as_dict": true, - "eve/tests/config.py::TestConfig::test_unexisting_env_config": true, - "eve/tests/config.py::TestConfig::test_url_helpers": true, - "eve/tests/config.py::TestConfig::test_url_rules": true, - "eve/tests/config.py::TestConfig::test_validate_datecreated_in_schema": true, - "eve/tests/config.py::TestConfig::test_validate_domain_struct": true, - "eve/tests/config.py::TestConfig::test_validate_invalid_field_names": true, - "eve/tests/config.py::TestConfig::test_validate_item_methods": true, - "eve/tests/config.py::TestConfig::test_validate_lastupdated_in_schema": true, - "eve/tests/config.py::TestConfig::test_validate_resource_methods": true, - "eve/tests/config.py::TestConfig::test_validate_roles": true, - "eve/tests/config.py::TestConfig::test_validate_schema": true, - "eve/tests/config.py::TestConfig::test_validate_schema_item_methods": true, - "eve/tests/config.py::TestConfig::test_validate_schema_methods": true, - "eve/tests/endpoints.py::TestCustomConverters::test_delete_uuid": true, - "eve/tests/endpoints.py::TestCustomConverters::test_get_uuid": true, - "eve/tests/endpoints.py::TestCustomConverters::test_patch_uuid": true, - "eve/tests/endpoints.py::TestCustomConverters::test_post_uuid": true, - "eve/tests/endpoints.py::TestCustomConverters::test_put_uuid": true, - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix": true, - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_post_internal": true, - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version": true, - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version_hateoas_links": true, - "eve/tests/endpoints.py::TestEndPoints::test_api_version": true, - "eve/tests/endpoints.py::TestEndPoints::test_homepage": true, - "eve/tests/endpoints.py::TestEndPoints::test_homepage_does_not_have_internal_resources": true, - "eve/tests/endpoints.py::TestEndPoints::test_internal_endpoint": true, - "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_additional_lookup": true, - "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_id": true, - "eve/tests/endpoints.py::TestEndPoints::test_item_self_link": true, - "eve/tests/endpoints.py::TestEndPoints::test_nested_endpoint": true, - "eve/tests/endpoints.py::TestEndPoints::test_oplog_endpoint": true, - "eve/tests/endpoints.py::TestEndPoints::test_resource_endpoint": true, - "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint": true, - "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint_does_not_attempt_callable_serialization": true, - "eve/tests/endpoints.py::TestEndPoints::test_unknown_endpoints": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_config": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_mongo_url": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_auth_params_provided": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_options": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_port": true, - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_valid_port": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_get_media_can_leverage_projection": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_base_url": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete_projection": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_errors": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch_null": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_excluded_file_in_result": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended_excluded_file_in_result": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_put": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_return_url": true, - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_partial_media": true, - "eve/tests/io/media.py::TestMediaStorage::test_base_media_storage": true, - "eve/tests/io/mongo.py::TestMongoDriver::test_combine_queries": true, - "eve/tests/io/mongo.py::TestMongoDriver::test_delete_returns_status": true, - "eve/tests/io/mongo.py::TestMongoDriver::test_get_value_from_query": true, - "eve/tests/io/mongo.py::TestMongoDriver::test_json_encoder_class": true, - "eve/tests/io/mongo.py::TestMongoDriver::test_query_contains_field": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_dependencies_with_defaults": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_feature_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_feature_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_geojson_not_compilant": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_geometry_not_compilant": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_not_compilant": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_multilinestring_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_multipoint_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_multipolygon_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_point_coordinates_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_point_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_point_integer_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_point_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_success": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_reject_invalid_schema": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_unique_fail": true, - "eve/tests/io/mongo.py::TestMongoValidator::test_unique_success": true, - "eve/tests/io/mongo.py::TestPythonParser::test_And_BoolOp": true, - "eve/tests/io/mongo.py::TestPythonParser::test_Attribute": true, - "eve/tests/io/mongo.py::TestPythonParser::test_Eq": true, - "eve/tests/io/mongo.py::TestPythonParser::test_Gt": true, - "eve/tests/io/mongo.py::TestPythonParser::test_GtE": true, - "eve/tests/io/mongo.py::TestPythonParser::test_Lt": true, - "eve/tests/io/mongo.py::TestPythonParser::test_LtE": true, - "eve/tests/io/mongo.py::TestPythonParser::test_NotEq": true, - "eve/tests/io/mongo.py::TestPythonParser::test_ObjectId_Call": true, - "eve/tests/io/mongo.py::TestPythonParser::test_Or_BoolOp": true, - "eve/tests/io/mongo.py::TestPythonParser::test_bad_Expr": true, - "eve/tests/io/mongo.py::TestPythonParser::test_datetime_Call": true, - "eve/tests/io/mongo.py::TestPythonParser::test_nested_BoolOp": true, - "eve/tests/io/mongo.py::TestPythonParser::test_unparsed_statement": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_create_index_with_mongo_uri_and_prefix": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_delete_multidb": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_get_multidb": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_patch_multidb": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_post_multidb": true, - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_put_multidb": true, - "eve/tests/io/multi_mongo.py::TestMultiMongoAuth::test_get_multidb": true, - "eve/tests/logging.py::TestUtils::test_logging_info": true, - "eve/tests/methods/common.py::TestNormalizeDottedFields::test_normalize_dotted_fields": true, - "eve/tests/methods/common.py::TestOpLogEndpointDisabled::test_post_oplog": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_delete_oplog": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_oplog_hook": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_patch_oplog": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_basic_auth": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_hmac_auth": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_token_auth": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog_does_not_alter_document": true, - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_soft_delete_oplog": true, - "eve/tests/methods/common.py::TestSerializer::test_dbref_serialize_lists_of_lists": true, - "eve/tests/methods/common.py::TestSerializer::test_mongo_serializes": true, - "eve/tests/methods/common.py::TestSerializer::test_non_blocking_on_simple_field_serialization_exception": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_alongside_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_boolean": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_schema_of_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_typesavers": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_nested_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_typesavers": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_list_alongside_x_of_rules": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_lists_of_lists": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_null_dictionary": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_null_list": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_number": true, - "eve/tests/methods/common.py::TestSerializer::test_serialize_subdocument": true, - "eve/tests/methods/common.py::TestTickets::test_ticket_681": true, - "eve/tests/methods/delete.py::TestDelete::test_bulk_delete_id_field": true, - "eve/tests/methods/delete.py::TestDelete::test_delete": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_custom_idfield": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_different_resource": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_empty_resource": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_different_resource": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_write_concern": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_bad_etag": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_missing": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_non_existant": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource_with_override": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_subresource": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_subresource_item": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_unknown_item": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_with_post_override": true, - "eve/tests/methods/delete.py::TestDelete::test_delete_write_concern": true, - "eve/tests/methods/delete.py::TestDelete::test_deleteitem_internal": true, - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_disabled_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_missing_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestDelete::test_unknown_resource": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item_contacts": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource_contacts": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item_contacts": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_resource_contacts": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_resource": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_resource": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_dynamic_filter": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_resource": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_item": true, - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_resource": true, - "eve/tests/methods/delete.py::TestResourceSpecificSoftDelete::test_resource_specific_softdelete": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_bulk_delete_id_field": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_custom_idfield": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_different_resource": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_empty_resource": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_different_resource": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_write_concern": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_bad_etag": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_missing": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_non_existant": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource_with_override": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource_item": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_unknown_item": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_with_post_override": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_write_concern": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_deleteitem_internal": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_exclude_soft_deleted_documents_from_unique_checks": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_exclusive_projection": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_disabled_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_missing_enforce_ifmatch_disabled": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_multiple_softdelete": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_restore_softdeleted": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_caching": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_datalayer": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_db_fields": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_deleted_field": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_show_deleted": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_embedded_doc": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_get_response_skips_embedded_expansion": true, - "eve/tests/methods/delete.py::TestSoftDelete::test_unknown_resource": true, - "eve/tests/methods/get.py::TestEvents::test_get_after_aggregation_hook": true, - "eve/tests/methods/get.py::TestEvents::test_get_before_aggregation_hook": true, - "eve/tests/methods/get.py::TestEvents::test_on_fetched_item": true, - "eve/tests/methods/get.py::TestEvents::test_on_fetched_item_contacts": true, - "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource": true, - "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource_contacts": true, - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_item": true, - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_resource": true, - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_homepage": true, - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_item": true, - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_resource": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_item": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_resource": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_item_dynamic_filter": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter_12_chr_nonunicode_string": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_item": true, - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_resource": true, - "eve/tests/methods/get.py::TestGet::test_cache_control": true, - "eve/tests/methods/get.py::TestGet::test_cursor_extra_find": true, - "eve/tests/methods/get.py::TestGet::test_documents_missing_standard_date_fields": true, - "eve/tests/methods/get.py::TestGet::test_expires": true, - "eve/tests/methods/get.py::TestGet::test_get": true, - "eve/tests/methods/get.py::TestGet::test_get_aggregation_endpoint": true, - "eve/tests/methods/get.py::TestGet::test_get_aggregation_pagination": true, - "eve/tests/methods/get.py::TestGet::test_get_aggregation_parsing": true, - "eve/tests/methods/get.py::TestGet::test_get_aggregation_with_lists": true, - "eve/tests/methods/get.py::TestGet::test_get_allowed_filters_operators": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_auto_document_fields": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_embedded": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_hateoas_links": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_idfield": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_items": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_links": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_max_results": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_page": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_params": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_projection": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_sort": true, - "eve/tests/methods/get.py::TestGet::test_get_custom_where": true, - "eve/tests/methods/get.py::TestGet::test_get_default_sort": true, - "eve/tests/methods/get.py::TestGet::test_get_embedded": true, - "eve/tests/methods/get.py::TestGet::test_get_embedded_media": true, - "eve/tests/methods/get.py::TestGet::test_get_embedded_media_validate_rest_of_fields": true, - "eve/tests/methods/get.py::TestGet::test_get_empty_resource": true, - "eve/tests/methods/get.py::TestGet::test_get_idfield_doesnt_exist": true, - "eve/tests/methods/get.py::TestGet::test_get_ifmatch_disabled": true, - "eve/tests/methods/get.py::TestGet::test_get_ims_empty_resource": true, - "eve/tests/methods/get.py::TestGet::test_get_internal_page": true, - "eve/tests/methods/get.py::TestGet::test_get_invalid_idfield_cors": true, - "eve/tests/methods/get.py::TestGet::test_get_invalid_sort_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_invalid_where_fields": true, - "eve/tests/methods/get.py::TestGet::test_get_invalid_where_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_lookup_field_as_string": true, - "eve/tests/methods/get.py::TestGet::test_get_max_results": true, - "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist": true, - "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist_nested": true, - "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_unvalidated": true, - "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_validated": true, - "eve/tests/methods/get.py::TestGet::test_get_nested_resource": true, - "eve/tests/methods/get.py::TestGet::test_get_page": true, - "eve/tests/methods/get.py::TestGet::test_get_pagination_no_documents": true, - "eve/tests/methods/get.py::TestGet::test_get_paging_disabled_no_args": true, - "eve/tests/methods/get.py::TestGet::test_get_perform_count_on_pagination_disabled": true, - "eve/tests/methods/get.py::TestGet::test_get_projection": true, - "eve/tests/methods/get.py::TestGet::test_get_projection_consistent_etag": true, - "eve/tests/methods/get.py::TestGet::test_get_projection_noschema": true, - "eve/tests/methods/get.py::TestGet::test_get_projection_subdocument": true, - "eve/tests/methods/get.py::TestGet::test_get_query_bitwise_query_operators": true, - "eve/tests/methods/get.py::TestGet::test_get_query_in_links": true, - "eve/tests/methods/get.py::TestGet::test_get_reference_embedded_in_subdocuments": true, - "eve/tests/methods/get.py::TestGet::test_get_resource_title": true, - "eve/tests/methods/get.py::TestGet::test_get_same_collection_different_resource": true, - "eve/tests/methods/get.py::TestGet::test_get_server_exclude_projection_can_project_others": true, - "eve/tests/methods/get.py::TestGet::test_get_server_exlcude_projection_can_sniff": true, - "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_block_sniff": true, - "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_can_exclude": true, - "eve/tests/methods/get.py::TestGet::test_get_sort_comma_delimited_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_sort_disabled": true, - "eve/tests/methods/get.py::TestGet::test_get_sort_mongo_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_static_projection": true, - "eve/tests/methods/get.py::TestGet::test_get_subresource": true, - "eve/tests/methods/get.py::TestGet::test_get_subresource_with_custom_idfield": true, - "eve/tests/methods/get.py::TestGet::test_get_total_count_header": true, - "eve/tests/methods/get.py::TestGet::test_get_where_allowed_filters": true, - "eve/tests/methods/get.py::TestGet::test_get_where_disabled": true, - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_combined_date": true, - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_objectid_as_string": true, - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax": true, - "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax1": true, - "eve/tests/methods/get.py::TestGet::test_get_with_post_override": true, - "eve/tests/methods/get.py::TestGetItem::test_cache_control": true, - "eve/tests/methods/get.py::TestGetItem::test_disallowed_getitem": true, - "eve/tests/methods/get.py::TestGetItem::test_expires": true, - "eve/tests/methods/get.py::TestGetItem::test_get_with_post_override": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id_different_resource": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_integer": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_different_resource": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_self_href": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_custom_auto_document_fields": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_embedded": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_if_modified_since": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_if_none_match": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled_if_mod_since": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_internal_by_id": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_lookup_field_as_string": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_missing_standard_date_fields": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_noschema": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_projection": true, - "eve/tests/methods/get.py::TestGetItem::test_getitem_with_custom_idfield": true, - "eve/tests/methods/get.py::TestGetItem::test_subresource_getitem": true, - "eve/tests/methods/get.py::TestHead::test_head_home": true, - "eve/tests/methods/get.py::TestHead::test_head_item": true, - "eve/tests/methods/get.py::TestHead::test_head_resource": true, - "eve/tests/methods/patch.py::TestEvents::test_on_PATCH_dynamic_filter": true, - "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH": true, - "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH_contacts": true, - "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH": true, - "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH_contacts": true, - "eve/tests/methods/patch.py::TestEvents::test_on_update": true, - "eve/tests/methods/patch.py::TestEvents::test_on_update_contacts": true, - "eve/tests/methods/patch.py::TestEvents::test_on_updated": true, - "eve/tests/methods/patch.py::TestEvents::test_on_updated_contacts": true, - "eve/tests/methods/patch.py::TestPatch::test_by_name": true, - "eve/tests/methods/patch.py::TestPatch::test_id_field_in_document_fails": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled_enforce_ifmatch_disabled": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing": true, - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing_enforce_ifmatch_disabled": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_allow_unknown": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_bandwidth_saver": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_custom_idfield": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_datetime": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_on_origin_document": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_value_on_origin_document": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_dict": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header_enforce_ifmatch_disabled": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_integer": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_internal": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_list": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_list_as_array": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default_with_post_override": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_standard_date_fields": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_multiple_fields": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_nested": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_not_overwritten": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_nullable_missing": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_null_objectid": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_objectid": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_readonly_field_with_previous_document": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_referential_integrity": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_rows": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_string": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_subresource": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_to_resource_endpoint": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_type_coercion": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_with_post_override": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_fail": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_success": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded": true, - "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded_number_serialization": true, - "eve/tests/methods/patch.py::TestPatch::test_readonly_resource": true, - "eve/tests/methods/patch.py::TestPatch::test_unique_value": true, - "eve/tests/methods/patch.py::TestPatch::test_unknown_id": true, - "eve/tests/methods/patch.py::TestPatch::test_unknown_id_different_resource": true, - "eve/tests/methods/post.py::TestEvents::test_on_POST_post_resource": true, - "eve/tests/methods/post.py::TestEvents::test_on_insert": true, - "eve/tests/methods/post.py::TestEvents::test_on_insert_contacts": true, - "eve/tests/methods/post.py::TestEvents::test_on_inserted": true, - "eve/tests/methods/post.py::TestEvents::test_on_inserted_contacts": true, - "eve/tests/methods/post.py::TestEvents::test_on_post_POST": true, - "eve/tests/methods/post.py::TestEvents::test_on_pre_POST": true, - "eve/tests/methods/post.py::TestEvents::test_on_pre_POST_contacts": true, - "eve/tests/methods/post.py::TestPost::test_custom_date_updated": true, - "eve/tests/methods/post.py::TestPost::test_custom_etag_update_date": true, - "eve/tests/methods/post.py::TestPost::test_custom_issues": true, - "eve/tests/methods/post.py::TestPost::test_custom_status": true, - "eve/tests/methods/post.py::TestPost::test_dbref_post_referential_integrity": true, - "eve/tests/methods/post.py::TestPost::test_id_field_included_with_document": true, - "eve/tests/methods/post.py::TestPost::test_multi_post_invalid": true, - "eve/tests/methods/post.py::TestPost::test_multi_post_valid": true, - "eve/tests/methods/post.py::TestPost::test_post_allow_unknown": true, - "eve/tests/methods/post.py::TestPost::test_post_alternative_payload": true, - "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_media_list": true, - "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_multiple_keys": true, - "eve/tests/methods/post.py::TestPost::test_post_auto_create_lists": true, - "eve/tests/methods/post.py::TestPost::test_post_bandwidth_saver": true, - "eve/tests/methods/post.py::TestPost::test_post_bulk_insert_on_disabled_bulk": true, - "eve/tests/methods/post.py::TestPost::test_post_custom_idfield": true, - "eve/tests/methods/post.py::TestPost::test_post_custom_json_content_type": true, - "eve/tests/methods/post.py::TestPost::test_post_datetime": true, - "eve/tests/methods/post.py::TestPost::test_post_decimal_number_fail": true, - "eve/tests/methods/post.py::TestPost::test_post_decimal_number_success": true, - "eve/tests/methods/post.py::TestPost::test_post_default_value": true, - "eve/tests/methods/post.py::TestPost::test_post_default_value_none": true, - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_default": true, - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_subdocuments": true, - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_values": true, - "eve/tests/methods/post.py::TestPost::test_post_dependency_required_fields": true, - "eve/tests/methods/post.py::TestPost::test_post_dict": true, - "eve/tests/methods/post.py::TestPost::test_post_duplicate_key": true, - "eve/tests/methods/post.py::TestPost::test_post_empty_bulk_insert": true, - "eve/tests/methods/post.py::TestPost::test_post_empty_resource": true, - "eve/tests/methods/post.py::TestPost::test_post_error_as_list": true, - "eve/tests/methods/post.py::TestPost::test_post_float_zero": true, - "eve/tests/methods/post.py::TestPost::test_post_ifmatch_disabled": true, - "eve/tests/methods/post.py::TestPost::test_post_integer": true, - "eve/tests/methods/post.py::TestPost::test_post_integer_zero": true, - "eve/tests/methods/post.py::TestPost::test_post_internal": true, - "eve/tests/methods/post.py::TestPost::test_post_internal_skip_validation": true, - "eve/tests/methods/post.py::TestPost::test_post_keyschema_dict": true, - "eve/tests/methods/post.py::TestPost::test_post_list": true, - "eve/tests/methods/post.py::TestPost::test_post_list_as_array": true, - "eve/tests/methods/post.py::TestPost::test_post_list_fixed_len": true, - "eve/tests/methods/post.py::TestPost::test_post_list_of_objectid": true, - "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_off": true, - "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_on": true, - "eve/tests/methods/post.py::TestPost::test_post_nested": true, - "eve/tests/methods/post.py::TestPost::test_post_nested_dict_objectid": true, - "eve/tests/methods/post.py::TestPost::test_post_null_objectid": true, - "eve/tests/methods/post.py::TestPost::test_post_objectid": true, - "eve/tests/methods/post.py::TestPost::test_post_readonly_field_with_default": true, - "eve/tests/methods/post.py::TestPost::test_post_readonly_in_dict": true, - "eve/tests/methods/post.py::TestPost::test_post_referential_integrity": true, - "eve/tests/methods/post.py::TestPost::test_post_referential_integrity_list": true, - "eve/tests/methods/post.py::TestPost::test_post_rows": true, - "eve/tests/methods/post.py::TestPost::test_post_string": true, - "eve/tests/methods/post.py::TestPost::test_post_to_item_endpoint": true, - "eve/tests/methods/post.py::TestPost::test_post_type_coercion": true, - "eve/tests/methods/post.py::TestPost::test_post_valueschema_dict": true, - "eve/tests/methods/post.py::TestPost::test_post_valueschema_with_objectid": true, - "eve/tests/methods/post.py::TestPost::test_post_with_content_type_charset": true, - "eve/tests/methods/post.py::TestPost::test_post_with_excluded_response_fields": true, - "eve/tests/methods/post.py::TestPost::test_post_with_extra_response_fields": true, - "eve/tests/methods/post.py::TestPost::test_post_with_get_override": true, - "eve/tests/methods/post.py::TestPost::test_post_with_relation_to_custom_idfield": true, - "eve/tests/methods/post.py::TestPost::test_post_write_concern": true, - "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded": true, - "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded_number_serialization": true, - "eve/tests/methods/post.py::TestPost::test_readonly_resource": true, - "eve/tests/methods/post.py::TestPost::test_subresource": true, - "eve/tests/methods/post.py::TestPost::test_subresource_required_ref": true, - "eve/tests/methods/post.py::TestPost::test_unknown_resource": true, - "eve/tests/methods/post.py::TestPost::test_validation_error": true, - "eve/tests/methods/put.py::TestEvents::test_on_post_PUT": true, - "eve/tests/methods/put.py::TestEvents::test_on_post_PUT_contacts": true, - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT": true, - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_contacts": true, - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_dynamic_filter": true, - "eve/tests/methods/put.py::TestEvents::test_on_replace": true, - "eve/tests/methods/put.py::TestEvents::test_on_replace_contacts": true, - "eve/tests/methods/put.py::TestEvents::test_on_replaced": true, - "eve/tests/methods/put.py::TestEvents::test_on_replaced_contacts": true, - "eve/tests/methods/put.py::TestPut::test_allow_unknown": true, - "eve/tests/methods/put.py::TestPut::test_by_name": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag_enforce_ifmatch_disabled": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled_enforce_ifmatch_disabled": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_missing": true, - "eve/tests/methods/put.py::TestPut::test_ifmatch_missing_enforce_ifmatch_disabled": true, - "eve/tests/methods/put.py::TestPut::test_put_bandwidth_saver": true, - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document": true, - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_fails_on_mismatching_id": true, - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_with_url_as_id": true, - "eve/tests/methods/put.py::TestPut::test_put_custom_idfield": true, - "eve/tests/methods/put.py::TestPut::test_put_dbref_subresource": true, - "eve/tests/methods/put.py::TestPut::test_put_default_value": true, - "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_default": true, - "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_wrong_value": true, - "eve/tests/methods/put.py::TestPut::test_put_etag_header": true, - "eve/tests/methods/put.py::TestPut::test_put_etag_header_enforce_ifmatch_disabled": true, - "eve/tests/methods/put.py::TestPut::test_put_internal": true, - "eve/tests/methods/put.py::TestPut::test_put_internal_skip_validation": true, - "eve/tests/methods/put.py::TestPut::test_put_nested": true, - "eve/tests/methods/put.py::TestPut::test_put_readonly_value_different": true, - "eve/tests/methods/put.py::TestPut::test_put_readonly_value_same": true, - "eve/tests/methods/put.py::TestPut::test_put_referential_integrity": true, - "eve/tests/methods/put.py::TestPut::test_put_referential_integrity_list": true, - "eve/tests/methods/put.py::TestPut::test_put_returns_404_on_unexisting_document": true, - "eve/tests/methods/put.py::TestPut::test_put_string": true, - "eve/tests/methods/put.py::TestPut::test_put_subresource": true, - "eve/tests/methods/put.py::TestPut::test_put_to_resource_endpoint": true, - "eve/tests/methods/put.py::TestPut::test_put_type_coercion": true, - "eve/tests/methods/put.py::TestPut::test_put_with_post_override": true, - "eve/tests/methods/put.py::TestPut::test_put_write_concern_fail": true, - "eve/tests/methods/put.py::TestPut::test_put_write_concern_success": true, - "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded": true, - "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded_number_serialization": true, - "eve/tests/methods/put.py::TestPut::test_readonly_resource": true, - "eve/tests/methods/put.py::TestPut::test_unique_value": true, - "eve/tests/methods/ratelimit.py::TestRateLimit::test_noratelimits": true, - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_home": true, - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_item": true, - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_resource": true, - "eve/tests/renders.py::TestRenders::test_CORS": true, - "eve/tests/renders.py::TestRenders::test_CORS_MAX_AGE": true, - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS": true, - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_item": true, - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_resources": true, - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_schema": true, - "eve/tests/renders.py::TestRenders::test_CORS_regex": true, - "eve/tests/renders.py::TestRenders::test_default_render": true, - "eve/tests/renders.py::TestRenders::test_json_disabled": true, - "eve/tests/renders.py::TestRenders::test_json_keys_sorted": true, - "eve/tests/renders.py::TestRenders::test_json_render": true, - "eve/tests/renders.py::TestRenders::test_json_xml_disabled": true, - "eve/tests/renders.py::TestRenders::test_jsonp_enabled": true, - "eve/tests/renders.py::TestRenders::test_unknown_render": true, - "eve/tests/renders.py::TestRenders::test_xml_disabled": true, - "eve/tests/renders.py::TestRenders::test_xml_leaf_escaping": true, - "eve/tests/renders.py::TestRenders::test_xml_ordered_nodes": true, - "eve/tests/renders.py::TestRenders::test_xml_render": true, - "eve/tests/renders.py::TestRenders::test_xml_url_escaping": true, - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage": true, - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage_reply": true, - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_item": true, - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_resource": true, - "eve/tests/response.py::TestNoHateoas::test_patch_no_hateoas": true, - "eve/tests/response.py::TestNoHateoas::test_post_no_hateoas": true, - "eve/tests/response.py::TestResponse::test_response_data": true, - "eve/tests/response.py::TestResponse::test_response_object": true, - "eve/tests/response.py::TestResponse::test_response_pretty": true, - "eve/tests/utils.py::TestUtils::test_date_to_str": true, - "eve/tests/utils.py::TestUtils::test_debug_error_message": true, - "eve/tests/utils.py::TestUtils::test_document_etag": true, - "eve/tests/utils.py::TestUtils::test_document_etag_ignore_fields": true, - "eve/tests/utils.py::TestUtils::test_extract_key_values": true, - "eve/tests/utils.py::TestUtils::test_import_from_string": true, - "eve/tests/utils.py::TestUtils::test_parse_request_if_match": true, - "eve/tests/utils.py::TestUtils::test_parse_request_if_modified_since": true, - "eve/tests/utils.py::TestUtils::test_parse_request_if_none_match": true, - "eve/tests/utils.py::TestUtils::test_parse_request_max_results": true, - "eve/tests/utils.py::TestUtils::test_parse_request_max_results_disabled_pagination": true, - "eve/tests/utils.py::TestUtils::test_parse_request_page": true, - "eve/tests/utils.py::TestUtils::test_parse_request_sort": true, - "eve/tests/utils.py::TestUtils::test_parse_request_where": true, - "eve/tests/utils.py::TestUtils::test_querydef": true, - "eve/tests/utils.py::TestUtils::test_str_to_date": true, - "eve/tests/utils.py::TestUtils::test_validate_filters": true, - "eve/tests/utils.py::TestUtils::test_weak_date": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_automatic_fields": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_delete": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_deleteitem": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_get": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_projection": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all_projection": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_bad_format": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_diffs": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_modified_since": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_none_match": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_pagination": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_unknown": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_multi_post": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item_contacts": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_patch": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_post": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_put": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_referential_integrity": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete_version_db_fields": true, - "eve/tests/versioning.py::TestCompleteVersioning::test_version_control_the_unkown": true, - "eve/tests/versioning.py::TestLateVersioning::test_datasource": true, - "eve/tests/versioning.py::TestLateVersioning::test_delete": true, - "eve/tests/versioning.py::TestLateVersioning::test_deleteitem": true, - "eve/tests/versioning.py::TestLateVersioning::test_embedded": true, - "eve/tests/versioning.py::TestLateVersioning::test_get": true, - "eve/tests/versioning.py::TestLateVersioning::test_getitem": true, - "eve/tests/versioning.py::TestLateVersioning::test_patch": true, - "eve/tests/versioning.py::TestLateVersioning::test_put": true, - "eve/tests/versioning.py::TestLateVersioning::test_referential_integrity": true, - "eve/tests/versioning.py::TestLateVersioning::test_softdelete": true, - "eve/tests/versioning.py::TestPartialVersioning::test_get": true, - "eve/tests/versioning.py::TestPartialVersioning::test_getitem": true, - "eve/tests/versioning.py::TestPartialVersioning::test_multi_post": true, - "eve/tests/versioning.py::TestPartialVersioning::test_patch": true, - "eve/tests/versioning.py::TestPartialVersioning::test_post": true, - "eve/tests/versioning.py::TestPartialVersioning::test_put": true, - "eve/tests/versioning.py::TestPartialVersioning::test_version_control_the_unkown": true, - "eve/tests/versioning.py::TestVersionedDataRelation::test_embedded": true, - "eve/tests/versioning.py::TestVersionedDataRelation::test_referential_integrity": true, - "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_data_relation_validation": true, - "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_embedded": true, - "eve/tests/versioning.py::TestVersionedDataRelationCustomField::test_referential_integrity": true, - "eve/tests/versioning.py::TestVersionedDataRelationUnversionedField::test_referential_integrity": true, - "eve/tests/versioning.py::TestVersioningWithCustomIdField::test_getitem": true, - "tests/__init__.py": true, - "tests/auth.py": true, - "tests/config.py": true, - "tests/endpoints.py": true, - "tests/io/__init__.py": true, - "tests/io/flask_pymongo.py": true, - "tests/io/media.py": true, - "tests/io/mongo.py": true, - "tests/io/multi_mongo.py": true, - "tests/logging.py": true, - "tests/methods/__init__.py": true, - "tests/methods/common.py": true, - "tests/methods/delete.py": true, - "tests/methods/get.py": true, - "tests/methods/patch.py": true, - "tests/methods/post.py": true, - "tests/methods/put.py": true, - "tests/methods/ratelimit.py": true, - "tests/renders.py": true, - "tests/response.py": true, - "tests/test_prefix.py": true, - "tests/test_prefix_version.py": true, - "tests/test_settings.py": true, - "tests/test_settings_env.py": true, - "tests/test_version.py": true, - "tests/utils.py": true, - "tests/versioning.py": true -} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids deleted file mode 100644 index 83c7d1a54..000000000 --- a/.pytest_cache/v/cache/nodeids +++ /dev/null @@ -1,779 +0,0 @@ -[ - "eve/tests/auth.py::TestBasicAuth::test_ALLOWED_ROLES_does_not_change", - "eve/tests/auth.py::TestBasicAuth::test_allowed_item_roles_does_not_change", - "eve/tests/auth.py::TestBasicAuth::test_allowed_roles_does_not_change", - "eve/tests/auth.py::TestBasicAuth::test_authorized_home_access", - "eve/tests/auth.py::TestBasicAuth::test_authorized_item_access", - "eve/tests/auth.py::TestBasicAuth::test_authorized_media_access", - "eve/tests/auth.py::TestBasicAuth::test_authorized_resource_access", - "eve/tests/auth.py::TestBasicAuth::test_authorized_schema_access", - "eve/tests/auth.py::TestBasicAuth::test_bad_auth_class", - "eve/tests/auth.py::TestBasicAuth::test_custom_auth", - "eve/tests/auth.py::TestBasicAuth::test_home_public_methods", - "eve/tests/auth.py::TestBasicAuth::test_instanced_auth", - "eve/tests/auth.py::TestBasicAuth::test_public_methods_but_locked_item", - "eve/tests/auth.py::TestBasicAuth::test_public_methods_but_locked_resource", - "eve/tests/auth.py::TestBasicAuth::test_public_methods_item", - "eve/tests/auth.py::TestBasicAuth::test_public_methods_resource", - "eve/tests/auth.py::TestBasicAuth::test_restricted_home_access", - "eve/tests/auth.py::TestBasicAuth::test_restricted_item_access", - "eve/tests/auth.py::TestBasicAuth::test_restricted_resource_access", - "eve/tests/auth.py::TestBasicAuth::test_rfc2617_response", - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_home_access", - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_item_access", - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_resource_access", - "eve/tests/auth.py::TestBasicAuth::test_unauthorized_schema_access", - "eve/tests/auth.py::TestTokenAuth::test_ALLOWED_ROLES_does_not_change", - "eve/tests/auth.py::TestTokenAuth::test_allowed_item_roles_does_not_change", - "eve/tests/auth.py::TestTokenAuth::test_allowed_roles_does_not_change", - "eve/tests/auth.py::TestTokenAuth::test_authorized_home_access", - "eve/tests/auth.py::TestTokenAuth::test_authorized_item_access", - "eve/tests/auth.py::TestTokenAuth::test_authorized_media_access", - "eve/tests/auth.py::TestTokenAuth::test_authorized_resource_access", - "eve/tests/auth.py::TestTokenAuth::test_authorized_schema_access", - "eve/tests/auth.py::TestTokenAuth::test_bad_auth_class", - "eve/tests/auth.py::TestTokenAuth::test_custom_auth", - "eve/tests/auth.py::TestTokenAuth::test_home_public_methods", - "eve/tests/auth.py::TestTokenAuth::test_instanced_auth", - "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_item", - "eve/tests/auth.py::TestTokenAuth::test_public_methods_but_locked_resource", - "eve/tests/auth.py::TestTokenAuth::test_public_methods_item", - "eve/tests/auth.py::TestTokenAuth::test_public_methods_resource", - "eve/tests/auth.py::TestTokenAuth::test_restricted_home_access", - "eve/tests/auth.py::TestTokenAuth::test_restricted_item_access", - "eve/tests/auth.py::TestTokenAuth::test_restricted_resource_access", - "eve/tests/auth.py::TestTokenAuth::test_rfc2617_response", - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_home_access", - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_item_access", - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_resource_access", - "eve/tests/auth.py::TestTokenAuth::test_unauthorized_schema_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_ALLOWED_ROLES_does_not_change", - "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_item_roles_does_not_change", - "eve/tests/auth.py::TestBearerTokenAuth::test_allowed_roles_does_not_change", - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_home_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_item_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_media_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_resource_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_authorized_schema_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_bad_auth_class", - "eve/tests/auth.py::TestBearerTokenAuth::test_custom_auth", - "eve/tests/auth.py::TestBearerTokenAuth::test_home_public_methods", - "eve/tests/auth.py::TestBearerTokenAuth::test_instanced_auth", - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_item", - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_but_locked_resource", - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_item", - "eve/tests/auth.py::TestBearerTokenAuth::test_public_methods_resource", - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_home_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_item_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_restricted_resource_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_rfc2617_response", - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_home_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_item_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_resource_access", - "eve/tests/auth.py::TestBearerTokenAuth::test_unauthorized_schema_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_ALLOWED_ROLES_does_not_change", - "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_item_roles_does_not_change", - "eve/tests/auth.py::TestCustomTokenAuth::test_allowed_roles_does_not_change", - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_home_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_item_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_media_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_resource_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_authorized_schema_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_bad_auth_class", - "eve/tests/auth.py::TestCustomTokenAuth::test_custom_auth", - "eve/tests/auth.py::TestCustomTokenAuth::test_home_public_methods", - "eve/tests/auth.py::TestCustomTokenAuth::test_instanced_auth", - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_item", - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_but_locked_resource", - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_item", - "eve/tests/auth.py::TestCustomTokenAuth::test_public_methods_resource", - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_home_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_item_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_restricted_resource_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_rfc2617_response", - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_home_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_item_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_resource_access", - "eve/tests/auth.py::TestCustomTokenAuth::test_unauthorized_schema_access", - "eve/tests/auth.py::TestHMACAuth::test_ALLOWED_ROLES_does_not_change", - "eve/tests/auth.py::TestHMACAuth::test_allowed_item_roles_does_not_change", - "eve/tests/auth.py::TestHMACAuth::test_allowed_roles_does_not_change", - "eve/tests/auth.py::TestHMACAuth::test_authorized_home_access", - "eve/tests/auth.py::TestHMACAuth::test_authorized_item_access", - "eve/tests/auth.py::TestHMACAuth::test_authorized_media_access", - "eve/tests/auth.py::TestHMACAuth::test_authorized_resource_access", - "eve/tests/auth.py::TestHMACAuth::test_authorized_schema_access", - "eve/tests/auth.py::TestHMACAuth::test_bad_auth_class", - "eve/tests/auth.py::TestHMACAuth::test_custom_auth", - "eve/tests/auth.py::TestHMACAuth::test_home_public_methods", - "eve/tests/auth.py::TestHMACAuth::test_instanced_auth", - "eve/tests/auth.py::TestHMACAuth::test_post_resource_hmac_auth", - "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_item", - "eve/tests/auth.py::TestHMACAuth::test_public_methods_but_locked_resource", - "eve/tests/auth.py::TestHMACAuth::test_public_methods_item", - "eve/tests/auth.py::TestHMACAuth::test_public_methods_resource", - "eve/tests/auth.py::TestHMACAuth::test_restricted_home_access", - "eve/tests/auth.py::TestHMACAuth::test_restricted_item_access", - "eve/tests/auth.py::TestHMACAuth::test_restricted_resource_access", - "eve/tests/auth.py::TestHMACAuth::test_rfc2617_response", - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_home_access", - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_item_access", - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_resource_access", - "eve/tests/auth.py::TestHMACAuth::test_unauthorized_schema_access", - "eve/tests/auth.py::TestResourceAuth::test_resource_only_auth", - "eve/tests/auth.py::TestUserRestrictedAccess::test_collection_get_public", - "eve/tests/auth.py::TestUserRestrictedAccess::test_delete", - "eve/tests/auth.py::TestUserRestrictedAccess::test_delete_item", - "eve/tests/auth.py::TestUserRestrictedAccess::test_filter_by_auth_field_id", - "eve/tests/auth.py::TestUserRestrictedAccess::test_get", - "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_criteria", - "eve/tests/auth.py::TestUserRestrictedAccess::test_get_by_auth_field_id", - "eve/tests/auth.py::TestUserRestrictedAccess::test_item_get_public", - "eve/tests/auth.py::TestUserRestrictedAccess::test_patch", - "eve/tests/auth.py::TestUserRestrictedAccess::test_post", - "eve/tests/auth.py::TestUserRestrictedAccess::test_post_bandwidth_saver_off_resource_auth", - "eve/tests/auth.py::TestUserRestrictedAccess::test_post_resource_auth", - "eve/tests/auth.py::TestUserRestrictedAccess::test_put", - "eve/tests/auth.py::TestUserRestrictedAccess::test_put_bandwidth_saver_off_resource_auth", - "eve/tests/auth.py::TestUserRestrictedAccess::test_put_resource_auth", - "eve/tests/auth.py::TestUserRestrictedAccess::test_unique_to_user_on_post", - "eve/tests/config.py::TestConfig::test_allow_unknown_with_soft_delete", - "eve/tests/config.py::TestConfig::test_auth_field_as_custom_idfield", - "eve/tests/config.py::TestConfig::test_auth_field_as_idfield", - "eve/tests/config.py::TestConfig::test_create_indexes", - "eve/tests/config.py::TestConfig::test_custom_datalayer", - "eve/tests/config.py::TestConfig::test_custom_error_handlers", - "eve/tests/config.py::TestConfig::test_custom_import_name", - "eve/tests/config.py::TestConfig::test_custom_kwargs", - "eve/tests/config.py::TestConfig::test_custom_validator", - "eve/tests/config.py::TestConfig::test_datasource", - "eve/tests/config.py::TestConfig::test_default_datalayer", - "eve/tests/config.py::TestConfig::test_default_import_name", - "eve/tests/config.py::TestConfig::test_default_settings", - "eve/tests/config.py::TestConfig::test_default_validator", - "eve/tests/config.py::TestConfig::test_existing_env_config", - "eve/tests/config.py::TestConfig::test_mongodb_settings", - "eve/tests/config.py::TestConfig::test_oplog_config", - "eve/tests/config.py::TestConfig::test_pretty_resource_urls", - "eve/tests/config.py::TestConfig::test_regexconverter", - "eve/tests/config.py::TestConfig::test_register_resource", - "eve/tests/config.py::TestConfig::test_set_defaults", - "eve/tests/config.py::TestConfig::test_set_schema_defaults", - "eve/tests/config.py::TestConfig::test_settings_as_dict", - "eve/tests/config.py::TestConfig::test_unexisting_env_config", - "eve/tests/config.py::TestConfig::test_url_helpers", - "eve/tests/config.py::TestConfig::test_url_rules", - "eve/tests/config.py::TestConfig::test_validate_datecreated_in_schema", - "eve/tests/config.py::TestConfig::test_validate_domain_struct", - "eve/tests/config.py::TestConfig::test_validate_invalid_field_names", - "eve/tests/config.py::TestConfig::test_validate_item_methods", - "eve/tests/config.py::TestConfig::test_validate_lastupdated_in_schema", - "eve/tests/config.py::TestConfig::test_validate_resource_methods", - "eve/tests/config.py::TestConfig::test_validate_roles", - "eve/tests/config.py::TestConfig::test_validate_schema", - "eve/tests/config.py::TestConfig::test_validate_schema_item_methods", - "eve/tests/config.py::TestConfig::test_validate_schema_methods", - "eve/tests/endpoints.py::TestCustomConverters::test_delete_uuid", - "eve/tests/endpoints.py::TestCustomConverters::test_get_uuid", - "eve/tests/endpoints.py::TestCustomConverters::test_patch_uuid", - "eve/tests/endpoints.py::TestCustomConverters::test_post_uuid", - "eve/tests/endpoints.py::TestCustomConverters::test_put_uuid", - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix", - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_post_internal", - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version", - "eve/tests/endpoints.py::TestEndPoints::test_api_prefix_version_hateoas_links", - "eve/tests/endpoints.py::TestEndPoints::test_api_version", - "eve/tests/endpoints.py::TestEndPoints::test_homepage", - "eve/tests/endpoints.py::TestEndPoints::test_homepage_does_not_have_internal_resources", - "eve/tests/endpoints.py::TestEndPoints::test_internal_endpoint", - "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_additional_lookup", - "eve/tests/endpoints.py::TestEndPoints::test_item_endpoint_id", - "eve/tests/endpoints.py::TestEndPoints::test_item_self_link", - "eve/tests/endpoints.py::TestEndPoints::test_nested_endpoint", - "eve/tests/endpoints.py::TestEndPoints::test_oplog_endpoint", - "eve/tests/endpoints.py::TestEndPoints::test_resource_endpoint", - "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint", - "eve/tests/endpoints.py::TestEndPoints::test_schema_endpoint_does_not_attempt_callable_serialization", - "eve/tests/endpoints.py::TestEndPoints::test_unknown_endpoints", - "eve/tests/logging.py::TestUtils::test_logging_info", - "eve/tests/renders.py::TestRenders::test_CORS", - "eve/tests/renders.py::TestRenders::test_CORS_MAX_AGE", - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS", - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_item", - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_resources", - "eve/tests/renders.py::TestRenders::test_CORS_OPTIONS_schema", - "eve/tests/renders.py::TestRenders::test_CORS_regex", - "eve/tests/renders.py::TestRenders::test_default_render", - "eve/tests/renders.py::TestRenders::test_json_disabled", - "eve/tests/renders.py::TestRenders::test_json_keys_sorted", - "eve/tests/renders.py::TestRenders::test_json_render", - "eve/tests/renders.py::TestRenders::test_json_xml_disabled", - "eve/tests/renders.py::TestRenders::test_jsonp_enabled", - "eve/tests/renders.py::TestRenders::test_unknown_render", - "eve/tests/renders.py::TestRenders::test_xml_disabled", - "eve/tests/renders.py::TestRenders::test_xml_leaf_escaping", - "eve/tests/renders.py::TestRenders::test_xml_ordered_nodes", - "eve/tests/renders.py::TestRenders::test_xml_render", - "eve/tests/renders.py::TestRenders::test_xml_url_escaping", - "eve/tests/response.py::TestResponse::test_response_data", - "eve/tests/response.py::TestResponse::test_response_object", - "eve/tests/response.py::TestResponse::test_response_pretty", - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage", - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_homepage_reply", - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_item", - "eve/tests/response.py::TestNoHateoas::test_get_no_hateoas_resource", - "eve/tests/response.py::TestNoHateoas::test_patch_no_hateoas", - "eve/tests/response.py::TestNoHateoas::test_post_no_hateoas", - "eve/tests/utils.py::TestUtils::test_date_to_str", - "eve/tests/utils.py::TestUtils::test_debug_error_message", - "eve/tests/utils.py::TestUtils::test_document_etag", - "eve/tests/utils.py::TestUtils::test_document_etag_ignore_fields", - "eve/tests/utils.py::TestUtils::test_extract_key_values", - "eve/tests/utils.py::TestUtils::test_import_from_string", - "eve/tests/utils.py::TestUtils::test_parse_request_if_match", - "eve/tests/utils.py::TestUtils::test_parse_request_if_modified_since", - "eve/tests/utils.py::TestUtils::test_parse_request_if_none_match", - "eve/tests/utils.py::TestUtils::test_parse_request_max_results", - "eve/tests/utils.py::TestUtils::test_parse_request_max_results_disabled_pagination", - "eve/tests/utils.py::TestUtils::test_parse_request_page", - "eve/tests/utils.py::TestUtils::test_parse_request_sort", - "eve/tests/utils.py::TestUtils::test_parse_request_where", - "eve/tests/utils.py::TestUtils::test_querydef", - "eve/tests/utils.py::TestUtils::test_str_to_date", - "eve/tests/utils.py::TestUtils::test_validate_filters", - "eve/tests/utils.py::TestUtils::test_weak_date", - "eve/tests/versioning.py::TestCompleteVersioning::test_automatic_fields", - "eve/tests/versioning.py::TestCompleteVersioning::test_delete", - "eve/tests/versioning.py::TestCompleteVersioning::test_deleteitem", - "eve/tests/versioning.py::TestCompleteVersioning::test_get", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_projection", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_all_projection", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_bad_format", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_diffs", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_modified_since", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_new_latest_version_invalidates_if_none_match", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_pagination", - "eve/tests/versioning.py::TestCompleteVersioning::test_getitem_version_unknown", - "eve/tests/versioning.py::TestCompleteVersioning::test_multi_post", - "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item", - "eve/tests/versioning.py::TestCompleteVersioning::test_on_fetched_item_contacts", - "eve/tests/versioning.py::TestCompleteVersioning::test_patch", - "eve/tests/versioning.py::TestCompleteVersioning::test_post", - "eve/tests/versioning.py::TestCompleteVersioning::test_put", - "eve/tests/versioning.py::TestCompleteVersioning::test_referential_integrity", - "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete", - "eve/tests/versioning.py::TestCompleteVersioning::test_softdelete_version_db_fields", - "eve/tests/versioning.py::TestCompleteVersioning::test_version_control_the_unkown", - "eve/tests/versioning.py::TestVersionedDataRelation::test_embedded", - "eve/tests/versioning.py::TestVersionedDataRelation::test_referential_integrity", - "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_data_relation_validation", - "eve/tests/versioning.py::TestVersionedDataRelation::test_softdelete_embedded", - "eve/tests/versioning.py::TestVersionedDataRelationCustomField::test_referential_integrity", - "eve/tests/versioning.py::TestVersionedDataRelationUnversionedField::test_referential_integrity", - "eve/tests/versioning.py::TestPartialVersioning::test_get", - "eve/tests/versioning.py::TestPartialVersioning::test_getitem", - "eve/tests/versioning.py::TestPartialVersioning::test_multi_post", - "eve/tests/versioning.py::TestPartialVersioning::test_patch", - "eve/tests/versioning.py::TestPartialVersioning::test_post", - "eve/tests/versioning.py::TestPartialVersioning::test_put", - "eve/tests/versioning.py::TestPartialVersioning::test_version_control_the_unkown", - "eve/tests/versioning.py::TestLateVersioning::test_datasource", - "eve/tests/versioning.py::TestLateVersioning::test_delete", - "eve/tests/versioning.py::TestLateVersioning::test_deleteitem", - "eve/tests/versioning.py::TestLateVersioning::test_embedded", - "eve/tests/versioning.py::TestLateVersioning::test_get", - "eve/tests/versioning.py::TestLateVersioning::test_getitem", - "eve/tests/versioning.py::TestLateVersioning::test_patch", - "eve/tests/versioning.py::TestLateVersioning::test_put", - "eve/tests/versioning.py::TestLateVersioning::test_referential_integrity", - "eve/tests/versioning.py::TestLateVersioning::test_softdelete", - "eve/tests/versioning.py::TestVersioningWithCustomIdField::test_getitem", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_config", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_auth_params_provided_in_mongo_url", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_auth_params_provided", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_options", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_invalid_port", - "eve/tests/io/flask_pymongo.py::TestPyMongo::test_valid_port", - "eve/tests/io/media.py::TestMediaStorage::test_base_media_storage", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_get_media_can_leverage_projection", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_base_url", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_delete_projection", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_errors", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_patch_null", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_excluded_file_in_result", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_post_extended_excluded_file_in_result", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_put", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_media_storage_return_url", - "eve/tests/io/media.py::TestGridFSMediaStorage::test_gridfs_partial_media", - "eve/tests/io/mongo.py::TestPythonParser::test_And_BoolOp", - "eve/tests/io/mongo.py::TestPythonParser::test_Attribute", - "eve/tests/io/mongo.py::TestPythonParser::test_Eq", - "eve/tests/io/mongo.py::TestPythonParser::test_Gt", - "eve/tests/io/mongo.py::TestPythonParser::test_GtE", - "eve/tests/io/mongo.py::TestPythonParser::test_Lt", - "eve/tests/io/mongo.py::TestPythonParser::test_LtE", - "eve/tests/io/mongo.py::TestPythonParser::test_NotEq", - "eve/tests/io/mongo.py::TestPythonParser::test_ObjectId_Call", - "eve/tests/io/mongo.py::TestPythonParser::test_Or_BoolOp", - "eve/tests/io/mongo.py::TestPythonParser::test_bad_Expr", - "eve/tests/io/mongo.py::TestPythonParser::test_datetime_Call", - "eve/tests/io/mongo.py::TestPythonParser::test_nested_BoolOp", - "eve/tests/io/mongo.py::TestPythonParser::test_unparsed_statement", - "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_dbref_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_decimal_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_dependencies_with_defaults", - "eve/tests/io/mongo.py::TestMongoValidator::test_feature_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_feature_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_featurecollection_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_geojson_not_compilant", - "eve/tests/io/mongo.py::TestMongoValidator::test_geometry_not_compilant", - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_not_compilant", - "eve/tests/io/mongo.py::TestMongoValidator::test_geometrycollection_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_linestring_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_multilinestring_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_multipoint_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_multipolygon_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_objectid_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_point_coordinates_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_point_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_point_integer_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_point_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_polygon_success", - "eve/tests/io/mongo.py::TestMongoValidator::test_reject_invalid_schema", - "eve/tests/io/mongo.py::TestMongoValidator::test_unique_fail", - "eve/tests/io/mongo.py::TestMongoValidator::test_unique_success", - "eve/tests/io/mongo.py::TestMongoDriver::test_combine_queries", - "eve/tests/io/mongo.py::TestMongoDriver::test_delete_returns_status", - "eve/tests/io/mongo.py::TestMongoDriver::test_get_value_from_query", - "eve/tests/io/mongo.py::TestMongoDriver::test_json_encoder_class", - "eve/tests/io/mongo.py::TestMongoDriver::test_query_contains_field", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_create_index_with_mongo_uri_and_prefix", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_delete_multidb", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_get_multidb", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_patch_multidb", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_post_multidb", - "eve/tests/io/multi_mongo.py::TestMethodsAcrossMultiMongo::test_put_multidb", - "eve/tests/io/multi_mongo.py::TestMultiMongoAuth::test_get_multidb", - "eve/tests/methods/common.py::TestSerializer::test_dbref_serialize_lists_of_lists", - "eve/tests/methods/common.py::TestSerializer::test_mongo_serializes", - "eve/tests/methods/common.py::TestSerializer::test_non_blocking_on_simple_field_serialization_exception", - "eve/tests/methods/common.py::TestSerializer::test_serialize_alongside_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_boolean", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_schema_of_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_list_of_x_of_typesavers", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_nested_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_inside_x_of_typesavers", - "eve/tests/methods/common.py::TestSerializer::test_serialize_list_alongside_x_of_rules", - "eve/tests/methods/common.py::TestSerializer::test_serialize_lists_of_lists", - "eve/tests/methods/common.py::TestSerializer::test_serialize_null_dictionary", - "eve/tests/methods/common.py::TestSerializer::test_serialize_null_list", - "eve/tests/methods/common.py::TestSerializer::test_serialize_number", - "eve/tests/methods/common.py::TestSerializer::test_serialize_subdocument", - "eve/tests/methods/common.py::TestNormalizeDottedFields::test_normalize_dotted_fields", - "eve/tests/methods/common.py::TestOpLogEndpointDisabled::test_post_oplog", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_delete_oplog", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_oplog_hook", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_patch_oplog", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_basic_auth", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_hmac_auth", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_post_oplog_with_token_auth", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_put_oplog_does_not_alter_document", - "eve/tests/methods/common.py::TestOpLogEndpointEnabled::test_soft_delete_oplog", - "eve/tests/methods/common.py::TestTickets::test_ticket_681", - "eve/tests/methods/delete.py::TestDelete::test_bulk_delete_id_field", - "eve/tests/methods/delete.py::TestDelete::test_delete", - "eve/tests/methods/delete.py::TestDelete::test_delete_custom_idfield", - "eve/tests/methods/delete.py::TestDelete::test_delete_different_resource", - "eve/tests/methods/delete.py::TestDelete::test_delete_empty_resource", - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint", - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_different_resource", - "eve/tests/methods/delete.py::TestDelete::test_delete_from_resource_endpoint_write_concern", - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_bad_etag", - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_disabled", - "eve/tests/methods/delete.py::TestDelete::test_delete_ifmatch_missing", - "eve/tests/methods/delete.py::TestDelete::test_delete_non_existant", - "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource", - "eve/tests/methods/delete.py::TestDelete::test_delete_readonly_resource_with_override", - "eve/tests/methods/delete.py::TestDelete::test_delete_subresource", - "eve/tests/methods/delete.py::TestDelete::test_delete_subresource_item", - "eve/tests/methods/delete.py::TestDelete::test_delete_unknown_item", - "eve/tests/methods/delete.py::TestDelete::test_delete_with_post_override", - "eve/tests/methods/delete.py::TestDelete::test_delete_write_concern", - "eve/tests/methods/delete.py::TestDelete::test_deleteitem_internal", - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_disabled_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestDelete::test_ifmatch_missing_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestDelete::test_unknown_resource", - "eve/tests/methods/delete.py::TestSoftDelete::test_bulk_delete_id_field", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_custom_idfield", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_different_resource", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_empty_resource", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_different_resource", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_from_resource_endpoint_write_concern", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_bad_etag", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_disabled", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_ifmatch_missing", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_non_existant", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_readonly_resource_with_override", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_subresource_item", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_unknown_item", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_with_post_override", - "eve/tests/methods/delete.py::TestSoftDelete::test_delete_write_concern", - "eve/tests/methods/delete.py::TestSoftDelete::test_deleteitem_internal", - "eve/tests/methods/delete.py::TestSoftDelete::test_exclude_soft_deleted_documents_from_unique_checks", - "eve/tests/methods/delete.py::TestSoftDelete::test_exclusive_projection", - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_bad_etag_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_disabled_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestSoftDelete::test_ifmatch_missing_enforce_ifmatch_disabled", - "eve/tests/methods/delete.py::TestSoftDelete::test_multiple_softdelete", - "eve/tests/methods/delete.py::TestSoftDelete::test_restore_softdeleted", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_caching", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_datalayer", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_db_fields", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_deleted_field", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdelete_show_deleted", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_embedded_doc", - "eve/tests/methods/delete.py::TestSoftDelete::test_softdeleted_get_response_skips_embedded_expansion", - "eve/tests/methods/delete.py::TestSoftDelete::test_unknown_resource", - "eve/tests/methods/delete.py::TestResourceSpecificSoftDelete::test_resource_specific_softdelete", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_item_contacts", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_delete_resource_contacts", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_item_contacts", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_deleted_resource_contacts", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_for_resource", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_post_DELETE_resource_for_resource", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_dynamic_filter", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_for_resource", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_item", - "eve/tests/methods/delete.py::TestDeleteEvents::test_on_pre_DELETE_resource_for_resource", - "eve/tests/methods/get.py::TestGet::test_cache_control", - "eve/tests/methods/get.py::TestGet::test_cursor_extra_find", - "eve/tests/methods/get.py::TestGet::test_documents_missing_standard_date_fields", - "eve/tests/methods/get.py::TestGet::test_expires", - "eve/tests/methods/get.py::TestGet::test_get", - "eve/tests/methods/get.py::TestGet::test_get_aggregation_endpoint", - "eve/tests/methods/get.py::TestGet::test_get_aggregation_pagination", - "eve/tests/methods/get.py::TestGet::test_get_aggregation_parsing", - "eve/tests/methods/get.py::TestGet::test_get_aggregation_with_lists", - "eve/tests/methods/get.py::TestGet::test_get_allowed_filters_operators", - "eve/tests/methods/get.py::TestGet::test_get_custom_auto_document_fields", - "eve/tests/methods/get.py::TestGet::test_get_custom_embedded", - "eve/tests/methods/get.py::TestGet::test_get_custom_hateoas_links", - "eve/tests/methods/get.py::TestGet::test_get_custom_idfield", - "eve/tests/methods/get.py::TestGet::test_get_custom_items", - "eve/tests/methods/get.py::TestGet::test_get_custom_links", - "eve/tests/methods/get.py::TestGet::test_get_custom_max_results", - "eve/tests/methods/get.py::TestGet::test_get_custom_page", - "eve/tests/methods/get.py::TestGet::test_get_custom_params", - "eve/tests/methods/get.py::TestGet::test_get_custom_projection", - "eve/tests/methods/get.py::TestGet::test_get_custom_sort", - "eve/tests/methods/get.py::TestGet::test_get_custom_where", - "eve/tests/methods/get.py::TestGet::test_get_default_sort", - "eve/tests/methods/get.py::TestGet::test_get_embedded", - "eve/tests/methods/get.py::TestGet::test_get_embedded_media", - "eve/tests/methods/get.py::TestGet::test_get_embedded_media_validate_rest_of_fields", - "eve/tests/methods/get.py::TestGet::test_get_empty_resource", - "eve/tests/methods/get.py::TestGet::test_get_idfield_doesnt_exist", - "eve/tests/methods/get.py::TestGet::test_get_ifmatch_disabled", - "eve/tests/methods/get.py::TestGet::test_get_ims_empty_resource", - "eve/tests/methods/get.py::TestGet::test_get_internal_page", - "eve/tests/methods/get.py::TestGet::test_get_invalid_idfield_cors", - "eve/tests/methods/get.py::TestGet::test_get_invalid_sort_syntax", - "eve/tests/methods/get.py::TestGet::test_get_invalid_where_fields", - "eve/tests/methods/get.py::TestGet::test_get_invalid_where_syntax", - "eve/tests/methods/get.py::TestGet::test_get_lookup_field_as_string", - "eve/tests/methods/get.py::TestGet::test_get_max_results", - "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist", - "eve/tests/methods/get.py::TestGet::test_get_mongo_query_blacklist_nested", - "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_unvalidated", - "eve/tests/methods/get.py::TestGet::test_get_nested_filter_operators_validated", - "eve/tests/methods/get.py::TestGet::test_get_nested_resource", - "eve/tests/methods/get.py::TestGet::test_get_page", - "eve/tests/methods/get.py::TestGet::test_get_pagination_no_documents", - "eve/tests/methods/get.py::TestGet::test_get_paging_disabled_no_args", - "eve/tests/methods/get.py::TestGet::test_get_perform_count_on_pagination_disabled", - "eve/tests/methods/get.py::TestGet::test_get_projection", - "eve/tests/methods/get.py::TestGet::test_get_projection_consistent_etag", - "eve/tests/methods/get.py::TestGet::test_get_projection_noschema", - "eve/tests/methods/get.py::TestGet::test_get_projection_subdocument", - "eve/tests/methods/get.py::TestGet::test_get_query_bitwise_query_operators", - "eve/tests/methods/get.py::TestGet::test_get_query_in_links", - "eve/tests/methods/get.py::TestGet::test_get_reference_embedded_in_subdocuments", - "eve/tests/methods/get.py::TestGet::test_get_resource_title", - "eve/tests/methods/get.py::TestGet::test_get_same_collection_different_resource", - "eve/tests/methods/get.py::TestGet::test_get_server_exclude_projection_can_project_others", - "eve/tests/methods/get.py::TestGet::test_get_server_exlcude_projection_can_sniff", - "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_block_sniff", - "eve/tests/methods/get.py::TestGet::test_get_server_include_projection_can_exclude", - "eve/tests/methods/get.py::TestGet::test_get_sort_comma_delimited_syntax", - "eve/tests/methods/get.py::TestGet::test_get_sort_disabled", - "eve/tests/methods/get.py::TestGet::test_get_sort_mongo_syntax", - "eve/tests/methods/get.py::TestGet::test_get_static_projection", - "eve/tests/methods/get.py::TestGet::test_get_subresource", - "eve/tests/methods/get.py::TestGet::test_get_subresource_with_custom_idfield", - "eve/tests/methods/get.py::TestGet::test_get_total_count_header", - "eve/tests/methods/get.py::TestGet::test_get_where_allowed_filters", - "eve/tests/methods/get.py::TestGet::test_get_where_disabled", - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_combined_date", - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_objectid_as_string", - "eve/tests/methods/get.py::TestGet::test_get_where_mongo_syntax", - "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax", - "eve/tests/methods/get.py::TestGet::test_get_where_python_syntax1", - "eve/tests/methods/get.py::TestGet::test_get_with_post_override", - "eve/tests/methods/get.py::TestGetItem::test_cache_control", - "eve/tests/methods/get.py::TestGetItem::test_disallowed_getitem", - "eve/tests/methods/get.py::TestGetItem::test_expires", - "eve/tests/methods/get.py::TestGetItem::test_get_with_post_override", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_id_different_resource", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_integer", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_different_resource", - "eve/tests/methods/get.py::TestGetItem::test_getitem_by_name_self_href", - "eve/tests/methods/get.py::TestGetItem::test_getitem_custom_auto_document_fields", - "eve/tests/methods/get.py::TestGetItem::test_getitem_embedded", - "eve/tests/methods/get.py::TestGetItem::test_getitem_if_modified_since", - "eve/tests/methods/get.py::TestGetItem::test_getitem_if_none_match", - "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled", - "eve/tests/methods/get.py::TestGetItem::test_getitem_ifmatch_disabled_if_mod_since", - "eve/tests/methods/get.py::TestGetItem::test_getitem_internal_by_id", - "eve/tests/methods/get.py::TestGetItem::test_getitem_lookup_field_as_string", - "eve/tests/methods/get.py::TestGetItem::test_getitem_missing_standard_date_fields", - "eve/tests/methods/get.py::TestGetItem::test_getitem_noschema", - "eve/tests/methods/get.py::TestGetItem::test_getitem_projection", - "eve/tests/methods/get.py::TestGetItem::test_getitem_with_custom_idfield", - "eve/tests/methods/get.py::TestGetItem::test_subresource_getitem", - "eve/tests/methods/get.py::TestHead::test_head_home", - "eve/tests/methods/get.py::TestHead::test_head_item", - "eve/tests/methods/get.py::TestHead::test_head_resource", - "eve/tests/methods/get.py::TestEvents::test_get_after_aggregation_hook", - "eve/tests/methods/get.py::TestEvents::test_get_before_aggregation_hook", - "eve/tests/methods/get.py::TestEvents::test_on_fetched_item", - "eve/tests/methods/get.py::TestEvents::test_on_fetched_item_contacts", - "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource", - "eve/tests/methods/get.py::TestEvents::test_on_fetched_resource_contacts", - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_item", - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_for_resource", - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_homepage", - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_item", - "eve/tests/methods/get.py::TestEvents::test_on_post_GET_resource_for_resource", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_item", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_for_resource", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_item_dynamic_filter", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_dynamic_filter_12_chr_nonunicode_string", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_item", - "eve/tests/methods/get.py::TestEvents::test_on_pre_GET_resource_for_resource", - "eve/tests/methods/patch.py::TestPatch::test_by_name", - "eve/tests/methods/patch.py::TestPatch::test_id_field_in_document_fails", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_bad_etag_enforce_ifmatch_disabled", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_disabled_enforce_ifmatch_disabled", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing", - "eve/tests/methods/patch.py::TestPatch::test_ifmatch_missing_enforce_ifmatch_disabled", - "eve/tests/methods/patch.py::TestPatch::test_patch_allow_unknown", - "eve/tests/methods/patch.py::TestPatch::test_patch_bandwidth_saver", - "eve/tests/methods/patch.py::TestPatch::test_patch_custom_idfield", - "eve/tests/methods/patch.py::TestPatch::test_patch_datetime", - "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_on_origin_document", - "eve/tests/methods/patch.py::TestPatch::test_patch_dependent_field_value_on_origin_document", - "eve/tests/methods/patch.py::TestPatch::test_patch_dict", - "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header", - "eve/tests/methods/patch.py::TestPatch::test_patch_etag_header_enforce_ifmatch_disabled", - "eve/tests/methods/patch.py::TestPatch::test_patch_integer", - "eve/tests/methods/patch.py::TestPatch::test_patch_internal", - "eve/tests/methods/patch.py::TestPatch::test_patch_list", - "eve/tests/methods/patch.py::TestPatch::test_patch_list_as_array", - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default", - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_default_with_post_override", - "eve/tests/methods/patch.py::TestPatch::test_patch_missing_standard_date_fields", - "eve/tests/methods/patch.py::TestPatch::test_patch_multiple_fields", - "eve/tests/methods/patch.py::TestPatch::test_patch_nested", - "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_not_overwritten", - "eve/tests/methods/patch.py::TestPatch::test_patch_nested_document_nullable_missing", - "eve/tests/methods/patch.py::TestPatch::test_patch_null_objectid", - "eve/tests/methods/patch.py::TestPatch::test_patch_objectid", - "eve/tests/methods/patch.py::TestPatch::test_patch_readonly_field_with_previous_document", - "eve/tests/methods/patch.py::TestPatch::test_patch_referential_integrity", - "eve/tests/methods/patch.py::TestPatch::test_patch_rows", - "eve/tests/methods/patch.py::TestPatch::test_patch_string", - "eve/tests/methods/patch.py::TestPatch::test_patch_subresource", - "eve/tests/methods/patch.py::TestPatch::test_patch_to_resource_endpoint", - "eve/tests/methods/patch.py::TestPatch::test_patch_type_coercion", - "eve/tests/methods/patch.py::TestPatch::test_patch_with_post_override", - "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_fail", - "eve/tests/methods/patch.py::TestPatch::test_patch_write_concern_success", - "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded", - "eve/tests/methods/patch.py::TestPatch::test_patch_x_www_form_urlencoded_number_serialization", - "eve/tests/methods/patch.py::TestPatch::test_readonly_resource", - "eve/tests/methods/patch.py::TestPatch::test_unique_value", - "eve/tests/methods/patch.py::TestPatch::test_unknown_id", - "eve/tests/methods/patch.py::TestPatch::test_unknown_id_different_resource", - "eve/tests/methods/patch.py::TestEvents::test_on_PATCH_dynamic_filter", - "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH", - "eve/tests/methods/patch.py::TestEvents::test_on_post_PATCH_contacts", - "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH", - "eve/tests/methods/patch.py::TestEvents::test_on_pre_PATCH_contacts", - "eve/tests/methods/patch.py::TestEvents::test_on_update", - "eve/tests/methods/patch.py::TestEvents::test_on_update_contacts", - "eve/tests/methods/patch.py::TestEvents::test_on_updated", - "eve/tests/methods/patch.py::TestEvents::test_on_updated_contacts", - "eve/tests/methods/post.py::TestPost::test_custom_date_updated", - "eve/tests/methods/post.py::TestPost::test_custom_etag_update_date", - "eve/tests/methods/post.py::TestPost::test_custom_issues", - "eve/tests/methods/post.py::TestPost::test_custom_status", - "eve/tests/methods/post.py::TestPost::test_dbref_post_referential_integrity", - "eve/tests/methods/post.py::TestPost::test_id_field_included_with_document", - "eve/tests/methods/post.py::TestPost::test_multi_post_invalid", - "eve/tests/methods/post.py::TestPost::test_multi_post_valid", - "eve/tests/methods/post.py::TestPost::test_post_allow_unknown", - "eve/tests/methods/post.py::TestPost::test_post_alternative_payload", - "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_media_list", - "eve/tests/methods/post.py::TestPost::test_post_auto_collapse_multiple_keys", - "eve/tests/methods/post.py::TestPost::test_post_auto_create_lists", - "eve/tests/methods/post.py::TestPost::test_post_bandwidth_saver", - "eve/tests/methods/post.py::TestPost::test_post_bulk_insert_on_disabled_bulk", - "eve/tests/methods/post.py::TestPost::test_post_custom_idfield", - "eve/tests/methods/post.py::TestPost::test_post_custom_json_content_type", - "eve/tests/methods/post.py::TestPost::test_post_datetime", - "eve/tests/methods/post.py::TestPost::test_post_decimal_number_fail", - "eve/tests/methods/post.py::TestPost::test_post_decimal_number_success", - "eve/tests/methods/post.py::TestPost::test_post_default_value", - "eve/tests/methods/post.py::TestPost::test_post_default_value_none", - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_default", - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_subdocuments", - "eve/tests/methods/post.py::TestPost::test_post_dependency_fields_with_values", - "eve/tests/methods/post.py::TestPost::test_post_dependency_required_fields", - "eve/tests/methods/post.py::TestPost::test_post_dict", - "eve/tests/methods/post.py::TestPost::test_post_duplicate_key", - "eve/tests/methods/post.py::TestPost::test_post_empty_bulk_insert", - "eve/tests/methods/post.py::TestPost::test_post_empty_resource", - "eve/tests/methods/post.py::TestPost::test_post_error_as_list", - "eve/tests/methods/post.py::TestPost::test_post_float_zero", - "eve/tests/methods/post.py::TestPost::test_post_ifmatch_disabled", - "eve/tests/methods/post.py::TestPost::test_post_integer", - "eve/tests/methods/post.py::TestPost::test_post_integer_zero", - "eve/tests/methods/post.py::TestPost::test_post_internal", - "eve/tests/methods/post.py::TestPost::test_post_internal_skip_validation", - "eve/tests/methods/post.py::TestPost::test_post_keyschema_dict", - "eve/tests/methods/post.py::TestPost::test_post_list", - "eve/tests/methods/post.py::TestPost::test_post_list_as_array", - "eve/tests/methods/post.py::TestPost::test_post_list_fixed_len", - "eve/tests/methods/post.py::TestPost::test_post_list_of_objectid", - "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_off", - "eve/tests/methods/post.py::TestPost::test_post_location_header_hateoas_on", - "eve/tests/methods/post.py::TestPost::test_post_nested", - "eve/tests/methods/post.py::TestPost::test_post_nested_dict_objectid", - "eve/tests/methods/post.py::TestPost::test_post_null_objectid", - "eve/tests/methods/post.py::TestPost::test_post_objectid", - "eve/tests/methods/post.py::TestPost::test_post_readonly_field_with_default", - "eve/tests/methods/post.py::TestPost::test_post_readonly_in_dict", - "eve/tests/methods/post.py::TestPost::test_post_referential_integrity", - "eve/tests/methods/post.py::TestPost::test_post_referential_integrity_list", - "eve/tests/methods/post.py::TestPost::test_post_rows", - "eve/tests/methods/post.py::TestPost::test_post_string", - "eve/tests/methods/post.py::TestPost::test_post_to_item_endpoint", - "eve/tests/methods/post.py::TestPost::test_post_type_coercion", - "eve/tests/methods/post.py::TestPost::test_post_valueschema_dict", - "eve/tests/methods/post.py::TestPost::test_post_valueschema_with_objectid", - "eve/tests/methods/post.py::TestPost::test_post_with_content_type_charset", - "eve/tests/methods/post.py::TestPost::test_post_with_excluded_response_fields", - "eve/tests/methods/post.py::TestPost::test_post_with_extra_response_fields", - "eve/tests/methods/post.py::TestPost::test_post_with_get_override", - "eve/tests/methods/post.py::TestPost::test_post_with_relation_to_custom_idfield", - "eve/tests/methods/post.py::TestPost::test_post_write_concern", - "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded", - "eve/tests/methods/post.py::TestPost::test_post_x_www_form_urlencoded_number_serialization", - "eve/tests/methods/post.py::TestPost::test_readonly_resource", - "eve/tests/methods/post.py::TestPost::test_subresource", - "eve/tests/methods/post.py::TestPost::test_subresource_required_ref", - "eve/tests/methods/post.py::TestPost::test_unknown_resource", - "eve/tests/methods/post.py::TestPost::test_validation_error", - "eve/tests/methods/post.py::TestEvents::test_on_POST_post_resource", - "eve/tests/methods/post.py::TestEvents::test_on_insert", - "eve/tests/methods/post.py::TestEvents::test_on_insert_contacts", - "eve/tests/methods/post.py::TestEvents::test_on_inserted", - "eve/tests/methods/post.py::TestEvents::test_on_inserted_contacts", - "eve/tests/methods/post.py::TestEvents::test_on_post_POST", - "eve/tests/methods/post.py::TestEvents::test_on_pre_POST", - "eve/tests/methods/post.py::TestEvents::test_on_pre_POST_contacts", - "eve/tests/methods/put.py::TestPut::test_allow_unknown", - "eve/tests/methods/put.py::TestPut::test_by_name", - "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag", - "eve/tests/methods/put.py::TestPut::test_ifmatch_bad_etag_enforce_ifmatch_disabled", - "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled", - "eve/tests/methods/put.py::TestPut::test_ifmatch_disabled_enforce_ifmatch_disabled", - "eve/tests/methods/put.py::TestPut::test_ifmatch_missing", - "eve/tests/methods/put.py::TestPut::test_ifmatch_missing_enforce_ifmatch_disabled", - "eve/tests/methods/put.py::TestPut::test_put_bandwidth_saver", - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document", - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_fails_on_mismatching_id", - "eve/tests/methods/put.py::TestPut::test_put_creates_unexisting_document_with_url_as_id", - "eve/tests/methods/put.py::TestPut::test_put_custom_idfield", - "eve/tests/methods/put.py::TestPut::test_put_dbref_subresource", - "eve/tests/methods/put.py::TestPut::test_put_default_value", - "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_default", - "eve/tests/methods/put.py::TestPut::test_put_dependency_fields_with_wrong_value", - "eve/tests/methods/put.py::TestPut::test_put_etag_header", - "eve/tests/methods/put.py::TestPut::test_put_etag_header_enforce_ifmatch_disabled", - "eve/tests/methods/put.py::TestPut::test_put_internal", - "eve/tests/methods/put.py::TestPut::test_put_internal_skip_validation", - "eve/tests/methods/put.py::TestPut::test_put_nested", - "eve/tests/methods/put.py::TestPut::test_put_readonly_value_different", - "eve/tests/methods/put.py::TestPut::test_put_readonly_value_same", - "eve/tests/methods/put.py::TestPut::test_put_referential_integrity", - "eve/tests/methods/put.py::TestPut::test_put_referential_integrity_list", - "eve/tests/methods/put.py::TestPut::test_put_returns_404_on_unexisting_document", - "eve/tests/methods/put.py::TestPut::test_put_string", - "eve/tests/methods/put.py::TestPut::test_put_subresource", - "eve/tests/methods/put.py::TestPut::test_put_to_resource_endpoint", - "eve/tests/methods/put.py::TestPut::test_put_type_coercion", - "eve/tests/methods/put.py::TestPut::test_put_with_post_override", - "eve/tests/methods/put.py::TestPut::test_put_write_concern_fail", - "eve/tests/methods/put.py::TestPut::test_put_write_concern_success", - "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded", - "eve/tests/methods/put.py::TestPut::test_put_x_www_form_urlencoded_number_serialization", - "eve/tests/methods/put.py::TestPut::test_readonly_resource", - "eve/tests/methods/put.py::TestPut::test_unique_value", - "eve/tests/methods/put.py::TestEvents::test_on_post_PUT", - "eve/tests/methods/put.py::TestEvents::test_on_post_PUT_contacts", - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT", - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_contacts", - "eve/tests/methods/put.py::TestEvents::test_on_pre_PUT_dynamic_filter", - "eve/tests/methods/put.py::TestEvents::test_on_replace", - "eve/tests/methods/put.py::TestEvents::test_on_replace_contacts", - "eve/tests/methods/put.py::TestEvents::test_on_replaced", - "eve/tests/methods/put.py::TestEvents::test_on_replaced_contacts", - "eve/tests/methods/ratelimit.py::TestRateLimit::test_noratelimits", - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_home", - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_item", - "eve/tests/methods/ratelimit.py::TestRateLimit::test_ratelimit_resource" -] \ No newline at end of file From cfe8d211c244226a70394116fd9b30cb035747e0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 7 May 2018 15:31:12 +0200 Subject: [PATCH 314/821] Switch to pytest; drop (dev-)requirements.txt --- CHANGES | 6 ++ CONTRIBUTING.rst | 209 +++++++++++++++++++++++++++++++++---------- dev-requirements.txt | 17 ---- docs/index.rst | 1 - docs/testing.rst | 188 -------------------------------------- pytest.ini | 1 + setup.py | 21 ++++- tox.ini | 3 +- 8 files changed, 189 insertions(+), 257 deletions(-) delete mode 100644 dev-requirements.txt delete mode 100644 docs/testing.rst diff --git a/CHANGES b/CHANGES index bcee810be..1e437b969 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,12 @@ Development Version 0.8 ~~~~~~~~~~~ +- Dev: Switch to pytest as the standard testing tool. +- Dev: Drop ``requiments.txt`` and ``dev-requirements.txt``. Use ``pip install + -e .[dev|tests|docs]`` instead. +- Docs: Comprehensive rewrite of the Contributing page. +- Docs: Drop the testing page; merge its contents with the Contributing + page. - Flask requirement set to >=1.0. Closes #1111. - Python 2.6 and Python 3.3 are no longer supported. - Tests: finally acknowledge the existence of modern APIs for both Mongo and diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index cdae72260..d0df3b24c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,50 +1,167 @@ -How to Contribute -################# +How to contribute +================= Contributions are welcome! Not familiar with the codebase yet? No problem! There are many ways to contribute to open source projects: reporting bugs, helping with the documentation, spreading the word and of course, adding new features and patches. -Getting Started ---------------- -#. Make sure you have a GitHub_ account. -#. Open a `new issue`_, assuming one does not already exist. -#. Clearly describe the issue including steps to reproduce when it is a bug. - -Making Changes --------------- -* Fork_ the repository on GitHub. -* Create a topic branch from where you want to base your work. -* This is usually the ``master`` branch. -* Make commits of logical units (if needed rebase your feature branch before - submitting it). -* Check for unnecessary whitespace with ``git diff --check`` before committing. -* Make sure your commit messages are in the `proper format`_. -* If your commit fixes an open issue, reference it in the commit message (#15). -* Make sure your code conforms to PEP8_ (we're using flake8_ for PEP8 and extra checks). -* Make sure you have added the necessary tests for your changes. -* Run all the tests to assure nothing else was accidentally broken. -* Run again the entire suite via tox_ to check your changes against multiple - python versions. ``pip install tox; tox`` -* Don't forget to add yourself to AUTHORS_. - -These guidelines also apply when helping with documentation (actually, -for typos and minor additions you might choose to `fork and -edit`_). See also the `running the tests`_ section in the official -documentation. - -Submitting Changes +Support questions +----------------- + +Please, don't use the issue tracker for this. Use one of the following +resources for questions about your own code: + +* Ask on `Stack Overflow`_. Search with Google first using: ``site:stackoverflow.com eve {search term, exception message, etc.}`` +* The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting feedback. +* The IRC channel ``#python-eve`` on FreeNode. + +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/eve?sort=linked +.. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve + +Reporting issues +---------------- + +- Describe what you expected to happen. +- If possible, include a `minimal, complete, and verifiable example`_ to help + us identify the issue. This also helps check that the issue is not with your + own code. +- Describe what actually happened. Include the full traceback if there was an + exception. +- List your Python and Eve versions. If possible, check if this issue is + already fixed in the repository. + +.. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve + +Submitting patches ------------------ -* Push your changes to a topic branch in your fork of the repository. -* Submit a `Pull Request`_. -* Wait for maintainer feedback. -Join us on IRC --------------- -If you're interested in contributing to the Eve project or have questions -about it come join us in our little #python-eve channel on irc.freenode.net. -It's comfy and cozy over there. +- Include tests if your patch is supposed to solve a bug, and explain + clearly under which circumstances the bug happens. Make sure the test fails + without your patch. +- Follow `PEP8`_. CI will reject a change that does not conform to the + guidelines. + +First time setup +~~~~~~~~~~~~~~~~ + +- Download and install the `latest version of git`_. +- Configure git with your `username`_ and `email`_:: + + git config --global user.name 'your name' + git config --global user.email 'your email' + +- Make sure you have a `GitHub account`_. +- Fork Eve to your GitHub account by clicking the `Fork`_ button. +- `Clone`_ your GitHub fork locally:: + + git clone https://github.com/{username}/eve + cd eve + +- Add the main repository as a remote to update later:: + + git remote add pyeve https://github.com/pyeve/eve + git fetch pyeve + +- Create a virtualenv:: + + python3 -m venv env + . env/bin/activate + # or "env\Scripts\activate" on Windows + +- Install Eve in editable mode with development dependencies:: + + pip install -e ".[dev]" + +.. _GitHub account: https://github.com/join +.. _latest version of git: https://git-scm.com/downloads +.. _username: https://help.github.com/articles/setting-your-username-in-git/ +.. _email: https://help.github.com/articles/setting-your-email-in-git/ +.. _Fork: https://github.com/pallets/flask/fork +.. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork + +Start coding +~~~~~~~~~~~~ + +- Create a branch to identify the issue you would like to work on (e.g. + ``fix_for_#1280``) +- Using your favorite editor, make your changes, `committing as you go`_. +- Follow `PEP8`_. +- Include tests that cover any code changes you make. Make sure the test fails + without your patch. `Run the tests. `_. +- Push your commits to GitHub and `create a pull request`_. +- Celebrate 🎉 + +.. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _PEP8: https://pep8.org/ +.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ + +.. _contributing-testsuite: + +Running the tests +~~~~~~~~~~~~~~~~~ + +Run the basic test suite with:: + + pytest + +If you want you can run a single module, say the ``methods`` suite:: + + pytest eve/tests/methods/ + +Or, to run only the ``get`` tests:: + + pytest eve/tests/methods/get.py + +You can also choose to just run a single class:: + + pytest eve/tests/methods/get.py::TestGet + +Or even a single test:: + + pytest eve/tests/methods/get.py::TestGet::test_get_emtpy_resource + +You can also collect tests by keyword:: + + pytest -k auth + +These only runs the tests for the current environment. Whether this is relevant +depends on which part of Eve you're working on. Travis-CI will run the full +suite when you submit your pull request. + +The full test suite takes a long time to run because it tests multiple +combinations of Python and dependencies. You need to have Python 2.7, 3.4, +3.5, 3.6, and PyPy installed to run all of the environments. Then run:: + + tox + +Or, if you want to only run your tests against a specific Python environment:: + + tox -e py36 + # py27 = Python 2.7 + # py34 = Python 3.4 + # py35 = Python 3.5 + # py36 = Python 3.6 + # pypy + PyPy + +Rate limiting tests +~~~~~~~~~~~~~~~~~~~ +While there are no test requirements for most of the suite, please be advised +that in order to execute the :ref:`ratelimiting` tests you need a running +Redis_ server. The Rate-Limiting tests are silently skipped if any of the two +conditions are not met. + +Building the docs +~~~~~~~~~~~~~~~~~ + +Build the docs in the ``docs`` directory using Sphinx:: + + cd docs + make html BUILDDIR=_build + +Open ``_build/html/index.html`` in your browser to view the docs. + +Read more about `Sphinx `_. First time contributor? ----------------------- @@ -54,21 +171,18 @@ Don't know where to start? -------------------------- There are usually several TODO comments scattered around the codebase, maybe check them out and see if you have ideas, or can help with them. Also, check -the `open issues`_ in case there's something that sparks your interest (there's -also a special ``contributor friendly`` label flagging some interesting feature -requests). And what about documentation? I suck at English so if you're fluent -with it (or notice any typo and/or mistake), why not help with that? In any -case, other than GitHub help_ pages, you might want to check this excellent -`Effective Guide to Pull Requests`_ +the `open issues`_ in case there's something that sparks your interest. And +what about documentation? I suck at English, so if you're fluent with it (or +notice any typo and/or mistake), why not help with that? In any case, other +than GitHub help_ pages, you might want to check this excellent `Effective +Guide to Pull Requests`_ .. _`the repository`: http://github.com/pyeve/eve .. _AUTHORS: https://github.com/pyeve/eve/blob/master/AUTHORS .. _`open issues`: https://github.com/pyeve/eve/issues .. _`new issue`: https://github.com/pyeve/eve/issues/new .. _GitHub: https://github.com/ -.. _Fork: https://help.github.com/articles/fork-a-repo .. _`proper format`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html -.. _PEP8: http://www.python.org/dev/peps/pep-0008/ .. _flake8: http://flake8.readthedocs.org/en/latest/ .. _tox: http://tox.readthedocs.org/en/latest/ .. _help: https://help.github.com/ @@ -76,5 +190,6 @@ case, other than GitHub help_ pages, you might want to check this excellent .. _`fork and edit`: https://github.com/blog/844-forking-with-the-edit-button .. _`Pull Request`: https://help.github.com/articles/creating-a-pull-request .. _`running the tests`: http://python-eve.org/testing#running-the-tests +.. _Redis: https://redis.io diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 75270836c..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -docutils==0.12 -flake8==2.3.0 -mccabe==0.3 -pep8==1.5.7 -pip-tools==0.3.5 -pip-review==0.4 -py==1.4.26 -pyflakes==0.8.1 -Pygments==2.0.1 -pytest==2.6.4 -redis==2.10.3 -Sphinx==1.2.3 -tox==2.4.1 -wheel==0.24.0 -testfixtures==4.1.2 -alabaster==0.7.10 -sphinxcontrib-embedly diff --git a/docs/index.rst b/docs/index.rst index f9580ab65..da9d3445a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,7 +98,6 @@ link `_. snippets/index extensions contributing - testing support updates authors diff --git a/docs/testing.rst b/docs/testing.rst deleted file mode 100644 index 92800649a..000000000 --- a/docs/testing.rst +++ /dev/null @@ -1,188 +0,0 @@ -Running the Tests -================= -Eve runs under Python 2.7, 3.4+, and PyPy. Therefore tests will be run in those -four platforms in our `continuous integration server`_. - -The easiest way to get started is to run the tests in your local environment -with: - -.. code-block:: console - - $ python setup.py test - -If you want you can run a single module, say the ``methods`` suite: - -.. code-block:: console - - $ python setup.py test -s eve.tests.methods - -Or, to run only the ``get`` tests: - -.. code-block:: console - - $ python setup.py test -s eve.tests.methods.get - -You can also choose to just run a single class: - -.. code-block:: console - - $ python setup.py test -s eve.tests.methods.get.TestGetItem - -Or even a single class function: - -.. code-block:: console - - $ python setup.py test -s eve.tests.methods.get.TestGetItem.test_expires - - -.. _test_prerequisites: - -Prerequisites -------------- - -Install the required dependencies for running tests and building documentation -by running :: - - $ pip install -r dev-requirements.txt - -Testing with other python versions ----------------------------------- -Before you submit a pull request, make sure your tests and changes run in -all supported python versions: 2.7, 3.4, 3.5, 3.6, and PyPy. Instead of -creating all those environments by hand, Eve uses tox_. - -Make sure you have all required python versions installed and run: - -.. code-block:: console - - $ pip install tox # First time only - $ tox - -This might take some time the first run as the different virtual environments -are created and dependencies are installed. If everything is ok, you will see -the following: - -.. code-block:: console - - _________ summary _________ - py27: commands succeeded - py34: commands succeeded - py35: commands succeeded - py36: commands succeeded - pypy: commands succeeded - flake8: commands succeeded - congratulations :) - -If something goes **wrong** and one test fails, you might need to run that test -in the specific python version. You can use the created environments to run -some specific tests. For example, if a test suite fails in Python 3.4: - -.. code-block:: console - - # From the project folder - $ tox -e py34 -- -s eve.tests.methods.get.TestGetItem - -Using Pytest -------------- -You also choose to run the whole test suite using pytest_: - -.. code-block:: console - - # Run the whole test suite - $ py.test - - # Run all tests in the 'methods' folder - $ py.test eve/tests/methods - - # Run all the tests named 'TestEvents' - $ py.test -k TestEvents - - # Run the specific test class - $ py.test eve/tests/methods/get.py::TestEvents - - # Run the specific test - $ py.test eve/tests/auth.py::TestBasicAuth::test_custom_auth - - -You can use pytest_ from tox_, but you will need to install it in the tox -environments before using it. - -.. code-block:: console - - $ .tox/py26/bin/pip install pytest - $ .tox/py26/bin/py.test - -Please note that, just for my own convenience, the ``pytest.ini`` file is -currently set up in such a way that any test run will abort after two failures. -Also, if you are a Vim_ user (you should), you might want to check out the awesome -pytest.vim_ plugin. - - -RateLimiting and Redis ----------------------- -While there are no test requirements for most of the suite, please be advised -that in order to execute the :ref:`ratelimiting` tests you need a running -Redis_ server, and redispy_ must be installed. The Rate-Limiting tests are -silently skipped if any of the two conditions are not met. - -Redispy will install automatically on the first test run, or you can install it -yourself with - -.. code-block:: console - - $ pip install redis - -Continuous Integration ----------------------- -Each time code is pushed to the ``master`` branch the whole test-suite is -executed on Travis-CI. This is also the case for pull-requests. When a pull -request is submitted and the CI run fails two things happen: a 'the build is -broken' email is sent to the submitter; the request is rejected. The -contributor can then fix the code, add one or more commits as needed, and push -again. - -The CI will also run flake8 so make sure that your code complies to PEP8 before -submitting a pull request, or be prepared to be mail-spammed by CI. - -Please note that in practice you're only supposed to submit pull requests -against the ``master`` branch, see :ref:`contributing`. - -Building documentation ----------------------- -Eve uses Sphinx_ for its documentation. To build the documentation locally, -switch to the ``docs`` folder and run :: - - $ make html - -This will generate html documentation in the folder ``~/code/eve.docs/html``, -which can be overridden with the ``BUILDDIR`` make variable :: - - $ make html BUILDDIR=/path/to/docs - -Make sure Sphinx_ reports no errors or warnings when running the above. - -To preview the documentation open ``index.html`` in the build directory :: - - $ open /path/to/docs/index.html - -Alternatively switch to the build directory, start a local webserver :: - - $ python3 -m http.server - -and then point your browser at ``localhost:8000``. - -.. note:: - - Eve uses a customised Sphinx_ theme based on alabaster_. The easiest way - to get the right version is by installing the :ref:`test_prerequisites`. - -.. _`continuous integration server`: https://travis-ci.org/pyeve/eve/ -.. _tox: http://tox.readthedocs.org/en/latest/ -.. _Redis: http://redis.io/ -.. _redispy: https://github.com/andymccurdy/redis-py -.. _simple: http://redis.io/topics/quickstart -.. _pytest: http://pytest.org -.. _pytest.vim: https://github.com/alfredodeza/pytest.vim -.. _Vim: http://en.wikipedia.org/wiki/Vim_(text_editor) -.. _Sphinx: http://sphinx-doc.org -.. _alabaster: https://pypi.python.org/pypi/alabaster diff --git a/pytest.ini b/pytest.ini index 04aeb8c88..bbd1d9ba4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] +testpaths=eve/tests python_files=eve/tests/*.py addopts = --maxfail=2 -rf --capture=no norecursedirs = testsuite .tox diff --git a/setup.py b/setup.py index 84620e04c..152d91006 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ with open('README.rst') as f: LONG_DESCRIPTION = f.read() -install_requires = [ +INSTALL_REQUIRES = [ 'cerberus>=1.1', 'events>=0.3,<0.4', 'flask>=1.0', @@ -16,6 +16,21 @@ 'simplejson>=3.3.0,<4.0', ] +EXTRAS_REQUIRE = { + "docs": [ + "sphinx", + "alabaster", + "sphinxcontrib-embedly" + ], + "tests": [ + "redis", + "testfixtures", + "pytest", + "tox", + ], +} +EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + setup( name='Eve', version='0.8-dev', @@ -33,8 +48,8 @@ platforms=["any"], packages=find_packages(), test_suite="eve.tests", - install_requires=install_requires, - tests_require=['redis', 'testfixtures'], + install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tox.ini b/tox.ini index 441643762..08d30d141 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,8 @@ envlist=py27,py34,py35,py36,pypy [testenv] -commands=python setup.py test {posargs} +extras=tests +commands=py.test eve {posargs} [testenv:flake8] deps=flake8 From bf21c4b40f31e907fe9bba7469c0b58acb21e0d2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 8 May 2018 10:43:24 +0200 Subject: [PATCH 315/821] Add Makefile for easy testing, docs, dev-install --- CHANGES | 5 +++-- CONTRIBUTING.rst | 13 +++++++++++-- Makefile | 31 +++++++++++++++++++++++++++++++ docs/Makefile | 4 ++-- 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 Makefile diff --git a/CHANGES b/CHANGES index 1e437b969..1bd504f80 100644 --- a/CHANGES +++ b/CHANGES @@ -8,12 +8,13 @@ Development Version 0.8 ~~~~~~~~~~~ +- Dev: Add a Makefile with shortcuts for testing, docs building, and + development install. - Dev: Switch to pytest as the standard testing tool. - Dev: Drop ``requiments.txt`` and ``dev-requirements.txt``. Use ``pip install -e .[dev|tests|docs]`` instead. - Docs: Comprehensive rewrite of the Contributing page. -- Docs: Drop the testing page; merge its contents with the Contributing - page. +- Docs: Drop the testing page; merge its contents with the Contributing page. - Flask requirement set to >=1.0. Closes #1111. - Python 2.6 and Python 3.3 are no longer supported. - Tests: finally acknowledge the existence of modern APIs for both Mongo and diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d0df3b24c..9fb1877f3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -153,16 +153,25 @@ conditions are not met. Building the docs ~~~~~~~~~~~~~~~~~ - Build the docs in the ``docs`` directory using Sphinx:: cd docs - make html BUILDDIR=_build + make html Open ``_build/html/index.html`` in your browser to view the docs. Read more about `Sphinx `_. +make targets +~~~~~~~~~~~~ +Eve provides a ``Makefile`` with various shortcuts. They will ensure that +all dependencies are installed. + +- ``make test`` runs the basic test suite with ``pytest`` +- ``make test-all`` runs the full test suite with ``tox`` +- ``make docs`` builds the HTML documentation +- ``make install-dev`` install Eve in editable mode with all development dependencies. + First time contributor? ----------------------- It's alright. We've all been there. See next chapter. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..dba183749 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +.PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload + +install-dev: + pip install -q -e .[dev] + +test: clean-pyc install-dev + pytest + +test-all: clean-pyc install-dev + tox + +tox: test-all + +BUILDDIR = _build +docs: install-dev + $(MAKE) -C docs html BUILDDIR=$(BUILDDIR) + +audit: + python setup.py audit + +clean-pyc: + @find . -name '*.pyc' -exec rm -f {} + + @find . -name '*.pyo' -exec rm -f {} + + @find . -name '*~' -exec rm -f {} + + +# Only useful on Nicola's own machine :-) +docs-upload: BUILDDIR = ~/code/eve.docs +docs-upload: docs + cd $(BUILDDIR)/html && \ + git commit -am "rebuild docs" && \ + git push diff --git a/docs/Makefile b/docs/Makefile index f78dddebd..103434533 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,8 +5,8 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = -#BUILDDIR = _build -BUILDDIR = ~/code/eve.docs +BUILDDIR = _build +#BUILDDIR = ~/code/eve.docs # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 From 0e575596265817e642c438ae77444325b4c4bd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Dietm=C3=BCller?= Date: Sat, 5 May 2018 16:09:34 +0200 Subject: [PATCH 316/821] Make merging of nested documents optional (enabled by default) Rationale --------- In the fix for #519, merging of nested documents on `PATCH` was introduced, essentially a `original.update(updates)` is performed. So far, the only way to avoid this is to use `PUT` (see #712), however, this is not always desirable. We have a use case where we only want to update a nested field of a document, therefore the appropriate HTML verb is `PATCH` since we do not want to replace the whole document. In this nested field, we need to remove a key, which, due to the merging, is currently impossible. Changes ------- - Introduce a new setting, `MERGE_NESTED_DOCUMENTS`, to control this behaviour globally or per-resource. Per default, this is set to `True`, so that nothing changes compares to the current version of Eve except when the configuration is changed explicitly. - Add a test - Add documentation --- AUTHORS | 2 +- docs/config.rst | 11 ++++++++++ eve/default_settings.py | 1 + eve/flaskapp.py | 2 ++ eve/methods/patch.py | 5 +++-- eve/tests/methods/patch.py | 43 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 971276f39..ab7f12e52 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Development Lead Patches and Contributions ````````````````````````` +- Alexander Dietmüller - Alexander Hendorf - Amedeo Bussi - Andreas Røssland @@ -122,7 +123,6 @@ Patches and Contributions - Nick Park - Nicolas Bazire - Nicolas Carlier -- NotSpecial - Olivier Carrère - Olivier Poitrey - Olof Johansson diff --git a/docs/config.rst b/docs/config.rst index 135955304..77f4e5ca3 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -754,6 +754,11 @@ uppercase. disable this feature, and a ``404`` will be returned instead. Defaults to ``True``. +``MERGE_NESTED_DOCUMENTS`` If ``True``, updates to nested fields are + merged with the current data on ``PATCH``. + If ``False``, the updates overwrite the + current data. Defaults to ``True``. + =================================== ========================================= .. _domain: @@ -1084,6 +1089,12 @@ always lowercase. :ref:`soft_delete` feature for this resource. Locally overrides ``SOFT_DELETE``. +``merge_nested_documents`` If ``True``, updates to nested fields are + merged with the current data on ``PATCH``. + If ``False``, the updates overwrite the + current data. Locally overrides + ``MERGE_NESTED_DOCUMENTS``. + =============================== =============================================== Here's an example of resource customization, mostly done by overriding global diff --git a/eve/default_settings.py b/eve/default_settings.py index aba35533f..2ca0472a5 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -204,6 +204,7 @@ ITEM_LOOKUP_FIELD = ID_FIELD ITEM_URL = 'regex("[a-f0-9]{24}")' UPSERT_ON_PUT = True # insert unexisting documents on PUT. +MERGE_NESTED_DOCUMENTS = True # use a simple file response format by default EXTENDED_MEDIA_INFO = [] diff --git a/eve/flaskapp.py b/eve/flaskapp.py index dca657455..6f0ac8fb6 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -646,6 +646,8 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault('hateoas', self.config['HATEOAS']) settings.setdefault('authentication', self.auth if self.auth else None) + settings.setdefault('merge_nested_documents', + self.config['MERGE_NESTED_DOCUMENTS']) # empty schemas are allowed for read-only access to resources schema = settings.setdefault('schema', {}) self.set_schema_defaults(schema, settings['id_field']) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index d63c9a795..553864209 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -193,8 +193,9 @@ def patch_internal(resource, payload=None, concurrency_check=False, getattr(app, "on_update")(resource, updates, original) getattr(app, "on_update_%s" % resource)(updates, original) - updates = resolve_nested_documents(updates, updated) - updated.update(updates) + if resource_def['merge_nested_documents']: + updates = resolve_nested_documents(updates, updated) + updated.update(updates) if config.IF_MATCH: resolve_document_etag(updated, resource) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 8e5c653fe..a4f545db8 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -544,6 +544,49 @@ def test_patch_nested_document_not_overwritten(self): self.assertEqual(test, 'default') self.assertEqual(int, 99) + def test_patch_nested_document_no_merge(self): + """ Test that nested documents are not merged, but overwritten, + if configured.""" + domain = { + 'merge_nested_documents': False, + 'schema': { + 'nested': { + 'type': 'dict', + } + } + } + self.app.config['BANDWIDTH_SAVER'] = False + self.app.register_resource('nomerge', domain) + + original = { + 'nested': { + 'key1': 'value1', + 'key2': 'value2', + } + } + changes = { + 'nested': { + 'key2': 'value2', + 'key3': 'value3', + } + } + + r, status = self.post("nomerge", data=original) + self.assert201(status) + + id = r['_id'] + etag = r['_etag'] + + r, status = self.patch( + "/%s/%s" % ('nomerge', id), + data=changes, + headers=[('If-Match', etag)] + ) + self.assert200(status) + + # Assert that nested document was completely overwritten + self.assertEqual(r['nested'], changes['nested']) + def test_patch_nested_document_nullable_missing(self): schema = { 'sensor': { From 66ccc1c8c98c9e9a76ffb5538efb8fac856ec6ba Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 9 May 2018 08:57:48 +0200 Subject: [PATCH 317/821] Changelog for #1140 --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 1bd504f80..10c80e809 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Development Version 0.8 ~~~~~~~~~~~ +- New: ``MERGE_NESTED_DOCUMENTS``. If ``True``, updates to nested fields are + merged with the current data on ``PATCH``. If ``False``, the updates + overwrite the current data. Defaults to ``True``. Addresses #519 (Alexander + Dietmüller). - Dev: Add a Makefile with shortcuts for testing, docs building, and development install. - Dev: Switch to pytest as the standard testing tool. From feb282574dfe82a395918a376cac48635ea4199e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 09:56:09 +0200 Subject: [PATCH 318/821] Acknowledge the v0.7.9 release --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 10c80e809..8ba8ee803 100644 --- a/CHANGES +++ b/CHANGES @@ -132,6 +132,13 @@ Breaking Changes Stable ------ +Version 0.7.9 +~~~~~~~~~~~~~ + +Released on May 10, 2018 + +- Python 2.6 and Python 3.3 are deprecated. Closes #1129. + Version 0.7.8 ~~~~~~~~~~~~~ From a2f1e9c837330b8e85c301a18bb6de99840fb956 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 09:41:01 +0200 Subject: [PATCH 319/821] Prepare changelog for v0.8 release - Add documnentation links where appropriate - Add tickets/pulls links where appropriate - Sort items: new, fix, dev, then docs (more or less) - Add note in the header, with link to the breaking changes section --- CHANGES | 236 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 146 insertions(+), 90 deletions(-) diff --git a/CHANGES b/CHANGES index 8ba8ee803..f2378d6d7 100644 --- a/CHANGES +++ b/CHANGES @@ -8,96 +8,90 @@ Development Version 0.8 ~~~~~~~~~~~ + +.. note:: + + Make sure you read the `Breaking Changes`_ section below. + +- New: support for `partial media requests`_. Clients can request partial file + downloads by adding a ``Range`` header to their media request (`#1050`_). +- New: `Renderer classes`_. ``RENDERER`` allows to change enabled renderers. + Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You + can create your own renderer by subclassing ``eve.render.Renderer``. Each + renderer should set valid mime attr and have ``.render()`` method + implemented. Please note that at least one renderer must always be enabled + (`#1092`_). +- New: ``on_delete_resource_originals`` fired when soft deletion occurs + (`#1030`_). +- New: ``before_aggregation`` and ``after_aggregation`` event hooks allow to + attach `custom callbacks to aggregation endpoints`_ (`#1057`_). +- New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful + when you need support for vendor-specific json types. Please note: responses + will still carry the standard ``application/json`` type. Defaults to + ``['application/json']`` (`#1024`_). +- New: when the media endpoint is enabled, the default authentication class + will be used to secure it. (`#1083`_; `#1049`_). - New: ``MERGE_NESTED_DOCUMENTS``. If ``True``, updates to nested fields are merged with the current data on ``PATCH``. If ``False``, the updates - overwrite the current data. Defaults to ``True``. Addresses #519 (Alexander - Dietmüller). -- Dev: Add a Makefile with shortcuts for testing, docs building, and - development install. -- Dev: Switch to pytest as the standard testing tool. -- Dev: Drop ``requiments.txt`` and ``dev-requirements.txt``. Use ``pip install - -e .[dev|tests|docs]`` instead. -- Docs: Comprehensive rewrite of the Contributing page. -- Docs: Drop the testing page; merge its contents with the Contributing page. -- Flask requirement set to >=1.0. Closes #1111. -- Python 2.6 and Python 3.3 are no longer supported. -- Tests: finally acknowledge the existence of modern APIs for both Mongo and - Python (get rid of most deprecation warnings). -- New: ``before_aggregation`` and ``after_aggregation`` event hooks allow to - attach custom callbacks to aggregation endpoints. Closes #1057. -- Fix: Crash with Cerberus 1.2. Closes #1137. -- Docs: Add link to the Eve course. It was authored by the project author, and - it is hosted by TalkPython Training. -- New: Add suport for mongo's ``$box`` geo query operator. Closes #1122. -- New: support for partial media requests. Clients can request partial file - downloads by adding a ``Range`` header to their media request (Marsch Huynh). - you should upgrade to Python 3 as soon as possible. Closes #1129. + overwrite the current data. Defaults to ``True`` (`#1140`_). +- New: support for MongoDB decimal type ``bson.decimal128.Decimal128`` + (`#1045`_). +- New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects + (`#769`_). +- New: Add support for MongoDB ``$box`` geo query operator (`#1122`_). +- New: ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` allows custom fields in GeoJSON + (`#1004`_). +- New: Add support for MongoDB ``$caseSensitive`` and ``$diactricSensitive`` + query operators (`#1126`_). +- New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, + ``$bitsAllSet``, ``$bitsAnyClear``, ``$bitsAnySet`` (`#1053`_). +- New: support for ``MONGO_AUTH_MECHANISM`` and + ``MONGO_AUTH_MECHANISM_PROPERTIES``. +- New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. + Previously, if ``MONGO_URI`` was missing the database name, an exception + would be rised (`#1037`_). +- Fix: OPLOG skipped even if ``OPLOG = True`` (`#1074`_). +- Fix: Cannot define default projection and request specific field. (`#1036`_). +- Fix: ``VALIDATE_FILTERS`` and ``ALLOWED_FILTERS`` do not work with + sub-document fields. (`#1123`_). +- Fix: Aggregation query parameter does not replace keys in the lists + (`#1025`_). +- Fix: serialization bug that randomly skips fields if "x_of" is encountered + (`#1042`_) - Fix: PUT behavior with User Restricted Resource Access. Ensure that, under every circumstance, users are unable to overwrite items owned by other users - (Luca Moretto). -- Fix: OPLOG skipped even if ``OPLOG = True``. Closes #1074 (Hung Le). -- Fix: Cannot define default projection and request specific field. Closes - #1036 (DHuan). -- Dev: pin testfixtures to v5.x as latest releases break on Python 2.6. - Addresses #1128. -- New: Add support for mongo's ``$caseSensitive`` and ``$diactricSensitive`` - query operators (Artem Kolesnikov). -- Fix: ``VALIDATE_FILTERS`` and ``ALLOWED_FILTERS`` do not work with - sub-document fields. Closes #1123 (Luca Moretto). -- Fix documentation typos (Olof Johansson) -- Fix a changelog typo (kreynen). + (`#1130`_). +- Fix: Crash with Cerberus 1.2 (`#1137`_). +- Fix documentation typos (`#1114`_, `#1102`_) - Fix: broken documentation links to Cerberus validation rules. -- New: Renderer classes. ``RENDERER`` allows to change enabled renderers. - Defaults to ``['eve.render.JSONRenderer', 'eve.render.XMLRenderer']``. You - can create your own renderer by subclassing ``eve.render.Renderer``. Each - renderer should set valid mime attr and have ``.render()`` method - implemented. Please note that at least one renderer must always be enabled - (Marcin Puhacz). -- Change: ``JSON`` and ``XML`` settings are deprecated and will be removed in - a future update. Use ``RENDERERS`` instead (Marcin Puhacz). -- New: Refactor index creation. We now have a new +- Fix: add sphinxcontrib-embedly to dev-requirements.txt. +- Fix: Removed OrderedDict dependency; use ``OrderedDict`` from + ``backport_collections`` instead (`#1070`_). +- Performance improved on retrieving a list of embedded documents (`#1029`_). +- Dev: Refactor index creation. We now have a new ``eve.io.mongo.ensure_mongo_indexes()`` function which ensures that eventual ``mongo_indexes`` defined for a resource are created on the active database. The function can be imported and invoked, for example in multi-db workflows where a db is activated based on the authenticated user performing the request (via custom auth classes). -- Fix: add sphinxcontrib-embedly to dev-requirements.txt. -- New: when the media endpoint is enabled, the default authentication class - will be used to secure it. Closes #1083. -- New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, - ``$bitsAllSet``, ``$bitsAnyClear``, ``$bitsAnySet``. Closes 1053 (Qiang - Zhang). -- Fix: Aggregation query parameter does not replace keys in the lists. Closes - #1025 (Serge Kir). -- Fix: Removed OrderedDict dependency; use ``OrderedDict`` from - ``backport_collections`` instead (Carl George). -- New: support fpr MongoDB decimal type ``bson.decimal128.Decimal128`` (Amedeo - Bussi). -- Update: upgrade PyMongo dependency to v3.5 (Amedeo Bussi). -- Fix: serialization bug that randomly skips fields if "x_of" is encountered. - See PR #1042 for details (Raychee). -- New: ``on_delete_resource_originals`` fired when soft deletion occurs (Amedeo - Bussi). -- New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. - Previously, if ``MONGO_URI`` was missing the database name, an exception - would be rised. Closes #1037. -- Performance improved on retrieving a list of embedded documents. Closes #1029 - (Amedeo Bussi). -- New: ``JSON_REQUEST_CONTENT_TYPES`` or supported JSON content types. Useful - when you need support for vendor-specific json types. Please note: responses - will still carry the standard ``application/json`` type. Defaults to - ``['application/json']``. Closes #1024. -- New: ``ALLOW_CUSTOM_FIELDS_IN_GEOJSON`` allows custom fields in GeoJSON - (Martin Fous). -- New: Support for ``Feature`` and ``FeatureCollection`` GeoJSON objects. - Closes #769 (Martin Fous). -- Config options ``MONGO_AUTH_MECHANISM`` and - ``MONGO_AUTH_MECHANISM_PROPERTIES`` added. -- Change: Support for Cerberus 1.0+. Closes #776 (Dominik Kellner, Brad P. - Crochet). -- Change: Drop Flask-PyMongo dependency. Closes #855 (Artem Kolesnikov). -- Change: ``DELETE`` on sub-resource endpoints will only delete the documents. - that match the endpoint semantics. Addresses #1010 (Amedeo Bussi). +- Dev: Add a `Makefile with shortcuts`_ for testing, docs building, and + development install. +- Dev: Switch to pytest as the standard testing tool. +- Dev: Drop ``requiments.txt`` and ``dev-requirements.txt``. Use ``pip install + -e .[dev|tests|docs]`` instead. +- Tests: finally acknowledge the existence of modern APIs for both Mongo and + Python (get rid of most deprecation warnings). +- Change: Support for Cerberus 1.0+ (`#776`_). +- Change: ``JSON`` and ``XML`` settings are deprecated and will be removed in + a future update. Use ``RENDERERS`` instead (`#1092`_). +- Flask dependency set to >=1.0 (`#1111`_). +- PyMongo dependency set to >=3.5. +- Events dependency set to >=v0.3. +- Drop Flask-PyMongo dependency, use custom code instead (`#855`_). +- Docs: Comprehensive rewrite of the `How to contribute`_ page. +- Docs: Drop the testing page; merge its contents with `How to contribute`_. +- Docs: Add link to the `Eve course`_. It was authored by the project author, + and it is hosted by TalkPython Training. - Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). - Dev: Delete and cleanup of some unnecessary code. - Dev: after the latest update (May 4th) travis-ci would not run tests on @@ -105,19 +99,35 @@ Version 0.8 - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. - Dev: fix insidious bug in ``tests.methods.post.TestPost`` class. -- Update: Upgrade Events dependency to v0.3. Breaking Changes ................ -- Eve now relies on `Cerberus `_ 1.0+, which allows for many new powerful validation and trasformation features (like `schema registries `_), improved performance and, in general, a more streamlined API. It also brings some notable breaking changes. - - ``keyschema`` was renamed to ``valueschema``, and ``propertyschema`` to ``keyschema``. - - A PATCH on a document which misses a field having a default value will now result in setting this value, even if the field was not provided in the PATCH's payload. - - Error messages for ``keyschema`` are now returned as dictionary. Example: ``{'a_dict': {'a_field': "value does not match regex '[a-z]+'"}}``. - - Error messages for type validations are `different now `_. - - It is no longer valid to have a field with ``default = None`` and ``nullable = False`` (see patch.py:test_patch_nested_document_nullable_missing). - - And more. A complete list of breaking changes is available `here `_. For detailed upgrade instructions, see Cerberus `upgrade notes `_. An in-depth analysis of changes made to the codebase (useful if you wrote a custom validator which needs to be upgraded) is available with `this commit message `_. - - Special thanks to Dominik Kellner and Brad P. Crochet for the amazing job done on this upgrade. +- Python 2.6 and Python 3.3 are no longer supported (`#1129`_). +- Eve now relies on `Cerberus`_ 1.1+ (`#776`_). It allows for many new + powerful validation and trasformation features (like `schema registries`_), + improved performance and, in general, a more streamlined API. It also brings + some notable breaking changes. + + - ``keyschema`` was renamed to ``valueschema``, and ``propertyschema`` to + ``keyschema``. + - A PATCH on a document which misses a field having a default value will + now result in setting this value, even if the field was not provided in + the PATCH's payload. + - Error messages for ``keyschema`` are now returned as dictionary. Example: + ``{'a_dict': {'a_field': "value does not match regex '[a-z]+'"}}``. + - Error messages for type validations are `different now`_. + - It is no longer valid to have a field with ``default = None`` and + ``nullable = False`` (see + *patch.py:test_patch_nested_document_nullable_missing*). + - And more. A complete list of breaking changes is available here_. For + detailed upgrade instructions, see Cerberus `upgrade notes`_. An in-depth + analysis of changes made to the codebase (useful if you wrote a custom + validator which needs to be upgraded) is available with `this commit + message`_. + - Special thanks to Dominik Kellner and Brad P. Crochet for the amazing job + done on this upgrade. + - Config setting ``MONGO_AUTHDBNAME`` renamed into ``MONGO_AUTH_SOURCE`` for naming consistency with PyMongo. - Config options ``MONGO_MAX_POOL_SIZE``, ``MONGO_SOCKET_TIMEOUT_MS``, @@ -126,8 +136,54 @@ Breaking Changes instead. - Be aware that ``DELETE`` on sub-resource endpoint will now only delete the documents matching endpoint semantics. A delete operation on - ``people/51f63e0838345b6dcd7eabff/invoices`` will delete all documents + ``people/51f63e0838345b6dcd7eabff/invoices`` will delete all documents matching the followig query: ``{'contact_id': '51f63e0838345b6dcd7eabff'}`` + (`#1010`_). + +.. _#1140: https://github.com/pyeve/eve/pull/1140 +.. _#1111: https://github.com/pyeve/eve/issues/1111 +.. _#1129: https://github.com/pyeve/eve/issues/1129 +.. _#1057: https://github.com/pyeve/eve/issues/1057 +.. _#1137: https://github.com/pyeve/eve/issues/1137 +.. _#1122: https://github.com/pyeve/eve/issues/1122 +.. _#1050: https://github.com/pyeve/eve/pull/1050 +.. _#1130: https://github.com/pyeve/eve/pull/1130 +.. _#1074: https://github.com/pyeve/eve/issues/1074 +.. _#1036: https://github.com/pyeve/eve/issues/1036 +.. _#1128: https://github.com/pyeve/eve/pull/1128 +.. _#1126: https://github.com/pyeve/eve/pull/1126 +.. _#1123: https://github.com/pyeve/eve/issues/1123 +.. _#1102: https://github.com/pyeve/eve/pull/1102 +.. _#1114: https://github.com/pyeve/eve/pull/1114 +.. _#1092: https://github.com/pyeve/eve/pull/1092 +.. _#1083: https://github.com/pyeve/eve/issues/1083 +.. _#1049: https://github.com/pyeve/eve/issues/1049 +.. _#1053: https://github.com/pyeve/eve/issues/1053 +.. _#1070: https://github.com/pyeve/eve/pull/1070 +.. _#1045: https://github.com/pyeve/eve/issues/1045 +.. _#1042: https://github.com/pyeve/eve/pull/1042 +.. _#1030: https://github.com/pyeve/eve/pull/1030 +.. _#1037: https://github.com/pyeve/eve/issues/1037 +.. _#1029: https://github.com/pyeve/eve/issues/1029 +.. _#1024: https://github.com/pyeve/eve/issues/1024 +.. _#769: https://github.com/pyeve/eve/issues/769 +.. _#1004: https://github.com/pyeve/eve/issues/1004 +.. _#776: https://github.com/pyeve/eve/issues/776 +.. _#855: https://github.com/pyeve/eve/issues/855 +.. _#1010: https://github.com/pyeve/eve/issues/1010 +.. _#1025: https://github.com/pyeve/eve/issues/1025 +.. _Cerberus: http://python-cerberus.org +.. _`schema registries`: http://docs.python-cerberus.org/en/stable/schemas.html#registries +.. _`different now`: http://docs.python-cerberus.org/en/stable/upgrading.html#data-types +.. _here: http://docs.python-cerberus.org/en/stable/changelog.html#breaking-changes +.. _`upgrade notes`: http://python-cerberus.org/en/stable/upgrading.html +.. _`this commit message`: https://github.com/pyeve/eve/pull/1001/commits/1110f807b478efa9f13ad1d217d22ceaa2a9e42d +.. _`partial media requests`: http://python-eve.org/features.html#partial-media-downloads +.. _`custom callbacks to aggregation endpoints`: http://python-eve.org/features.html#aggregation-event-hooks +.. _`Renderer classes`: http://python-eve.org/features.html#rendering +.. _`makefile with shortcuts`: http://python-eve.org/contributing.html#make-targets +.. _`How to contribute`: http://python-eve.org/contributing.html +.. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course Stable ------ From 774a6f04e4b6f2c1d9c0b3290ae38d509b91e622 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 10:01:22 +0200 Subject: [PATCH 320/821] Make 'funding' less intrusive in README --- README.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 0d1970c41..ee64e2750 100644 --- a/README.rst +++ b/README.rst @@ -8,18 +8,6 @@ allows to effortlessly build and deploy highly customizable, fully featured RESTful Web Services. Eve offers native support for MongoDB, and SQL backends via community extensions. -Funding -------- -Eve REST framework is a open source, collaboratively funded project. If you run -a business and are using Eve in a revenue-generating product, it would make -business sense to sponsor Eve development: it ensures the project that your -product relies on stays healthy and actively maintained. Individual users are -also welcome to make a recurring pledge or a one time donation if Eve has -helped you in your work or personal projects. - -Every single sign-up makes a significant impact towards making Eve possible. To -learn more, check out our `funding page`_. - Eve is Simple ------------- .. code-block:: python @@ -80,6 +68,17 @@ Features * MongoDB and SQL Support * Powered by Flask +Funding +------- +Eve REST framework is a open source, collaboratively funded project. If you run +a business and are using Eve in a revenue-generating product, it would make +business sense to sponsor Eve development: it ensures the project that your +product relies on stays healthy and actively maintained. Individual users are +also welcome to make a recurring pledge or a one time donation if Eve has +helped you in your work or personal projects. + +Every single sign-up makes a significant impact towards making Eve possible. To +learn more, check out our `funding page`_. License ------- From 06e1fe27adcaf2a44ca545d905e75bdd04769440 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 09:57:54 +0200 Subject: [PATCH 321/821] Bump version to 0.8 --- CHANGES | 8 +++++--- eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index f2378d6d7..49434d949 100644 --- a/CHANGES +++ b/CHANGES @@ -6,9 +6,14 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Stable +------ + Version 0.8 ~~~~~~~~~~~ +Released on May 10, 2018. + .. note:: Make sure you read the `Breaking Changes`_ section below. @@ -185,9 +190,6 @@ Breaking Changes .. _`How to contribute`: http://python-eve.org/contributing.html .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course -Stable ------- - Version 0.7.9 ~~~~~~~~~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index bb577c84e..2f8098b6d 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.8-dev' +__version__ = '0.8' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 152d91006..8b34a20c4 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name='Eve', - version='0.8-dev', + version='0.8', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 87a12259aa88e171fdc1761542253e6e378dcaf7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 15:47:29 +0200 Subject: [PATCH 322/821] Bump version to 0.8.1.dev0 --- CHANGES | 7 +++++++ eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 49434d949..71d4b5bb6 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. Development ----------- +Version 0.8.1 +~~~~~~~~~~~~~ + +Not released yet + +- hic sunt leones. + Stable ------ diff --git a/eve/__init__.py b/eve/__init__.py index 2f8098b6d..e3ce7809b 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = '0.8' +__version__ = '0.8.1.dev0' # RFC 1123 (ex RFC 822) DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' diff --git a/setup.py b/setup.py index 8b34a20c4..ab37bdc29 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name='Eve', - version='0.8', + version='0.8.1.dev0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From f00a1887e73782027d0c89b96d94fb84e540522b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 May 2018 15:56:32 +0200 Subject: [PATCH 323/821] Only set package version in one place (__init__.py) Closes #1142. --- CHANGES | 4 +++- setup.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 71d4b5bb6..506c4b450 100644 --- a/CHANGES +++ b/CHANGES @@ -11,7 +11,9 @@ Version 0.8.1 Not released yet -- hic sunt leones. +- Fix: Only set the package version in ``__init__.py`` (`#1142`_) + +.. _`#1142`: https://github.com/pyeve/eve/issues/1142 Stable ------ diff --git a/setup.py b/setup.py index ab37bdc29..645a43759 100755 --- a/setup.py +++ b/setup.py @@ -1,13 +1,18 @@ #!/usr/bin/env python -from collections import Counter, OrderedDict # noqa +import io +import re import importlib - from setuptools import setup, find_packages + DESCRIPTION = ("Python REST API for Humans.") with open('README.rst') as f: LONG_DESCRIPTION = f.read() +with io.open('eve/__init__.py', 'rt', encoding='utf8') as f: + VERSION = re.search(r'__version__ = \'(.*?)\'', f.read()).group(1) + + INSTALL_REQUIRES = [ 'cerberus>=1.1', 'events>=0.3,<0.4', @@ -33,7 +38,7 @@ setup( name='Eve', - version='0.8.1.dev0', + version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Nicola Iarocci', From 1f13c01cb39a2e9e590f588ee19b7fa150e1a9da Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 May 2018 09:30:46 +0200 Subject: [PATCH 324/821] Improve changelog format to increase readability. Closes #1143. --- CHANGES => CHANGES.rst | 29 ++++++++++++++--------------- MANIFEST.in | 2 +- docs/changelog.rst | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) rename CHANGES => CHANGES.rst (99%) diff --git a/CHANGES b/CHANGES.rst similarity index 99% rename from CHANGES rename to CHANGES.rst index 506c4b450..8da3c0174 100644 --- a/CHANGES +++ b/CHANGES.rst @@ -1,31 +1,29 @@ -Changelog -========= +Eve Changelog +============= Here you can see the full list of changes between each Eve release. -Development ------------ - Version 0.8.1 -~~~~~~~~~~~~~ +------------- -Not released yet +Unreleased -- Fix: Only set the package version in ``__init__.py`` (`#1142`_) +Improved +~~~~~~~~ +- Improve changelog format to reduce noise and increase readability. (`#1143`_) +- Only set the package version in ``__init__.py``. (`#1142`_) .. _`#1142`: https://github.com/pyeve/eve/issues/1142 - -Stable ------- +.. _`#1143`: https://github.com/pyeve/eve/issues/1143 Version 0.8 -~~~~~~~~~~~ +----------- Released on May 10, 2018. .. note:: - Make sure you read the `Breaking Changes`_ section below. + Make sure you read the :ref:`Breaking Changes ` section below. - New: support for `partial media requests`_. Clients can request partial file downloads by adding a ``Range`` header to their media request (`#1050`_). @@ -114,9 +112,10 @@ Released on May 10, 2018. being tested. - Dev: fix insidious bug in ``tests.methods.post.TestPost`` class. -Breaking Changes -................ +.. _breaking_changes: +Breaking Changes +~~~~~~~~~~~~~~~~ - Python 2.6 and Python 3.3 are no longer supported (`#1129`_). - Eve now relies on `Cerberus`_ 1.1+ (`#776`_). It allows for many new powerful validation and trasformation features (like `schema registries`_), diff --git a/MANIFEST.in b/MANIFEST.in index a0d08bae6..583289344 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include CHANGES LICENSE AUTHORS README.rst +include CHANGES.rst LICENSE AUTHORS README.rst recursive-include tests * recursive-include docs * recursive-include examples * diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b741623f..35d8df15c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,5 @@ .. _changelog: -.. include:: ../CHANGES +.. include:: ../CHANGES.rst From 2300269a1efff6e86f5bfee8da18e3e52e42a732 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 May 2018 09:41:57 +0200 Subject: [PATCH 325/821] Fix broken 'audit' command. - Replace the broken 'make audit' shortcut with 'make check' - Add the command to CONTRIBUTING.rst Closes #1144. --- CHANGES.rst | 6 ++++++ CONTRIBUTING.rst | 1 + Makefile | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8da3c0174..4403dd3c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,11 @@ Version 0.8.1 Unreleased +Fixed +~~~~~ +- Replace the broken ``make audit`` shortcut with ``make check``, then add the + command to ``CONTRIBUTING.rst`` where it is missing. (`#1144`_) + Improved ~~~~~~~~ - Improve changelog format to reduce noise and increase readability. (`#1143`_) @@ -15,6 +20,7 @@ Improved .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 +.. _`#1144`: https://github.com/pyeve/eve/issues/1144 Version 0.8 ----------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9fb1877f3..dee0228f3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -170,6 +170,7 @@ all dependencies are installed. - ``make test`` runs the basic test suite with ``pytest`` - ``make test-all`` runs the full test suite with ``tox`` - ``make docs`` builds the HTML documentation +- ``make check`` performs some checks on the package - ``make install-dev`` install Eve in editable mode with all development dependencies. First time contributor? diff --git a/Makefile b/Makefile index dba183749..7e793cb51 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,8 @@ BUILDDIR = _build docs: install-dev $(MAKE) -C docs html BUILDDIR=$(BUILDDIR) -audit: - python setup.py audit +check: + python setup.py check -r -s clean-pyc: @find . -name '*.pyc' -exec rm -f {} + From 4875a035730e5b671effe247f309758167e81e27 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 May 2018 11:20:40 +0200 Subject: [PATCH 326/821] Automatically close stale issues and PRs Install the Stale (Probot) GitHub app to automatically close stale issues and PRs. Currently: - A issue/PR is considered stale after 180 days with no activity. - A stale issue is flagged with the 'stale' label by the bot - A comment is added to the ticket to inform that the issue is stale and will be closed if no further activity happens soon - A stale issue/PR is closed after 7 days of inactivity. Closes #1145 --- .github/stale.yml | 2 ++ CHANGES.rst | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..7806c1c00 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,2 @@ +daysUntilStale: 180 +staleLabel: stale diff --git a/CHANGES.rst b/CHANGES.rst index 4403dd3c1..f6edd530c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,12 +15,14 @@ Fixed Improved ~~~~~~~~ +- Add a stale-bot to automatically close stale issues and pull requests (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) - Only set the package version in ``__init__.py``. (`#1142`_) .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 .. _`#1144`: https://github.com/pyeve/eve/issues/1144 +.. _`#1145`: https://github.com/pyeve/eve/issues/1145 Version 0.8 ----------- From 753310636a6c4c81cc8335fb4addd3cd9731f636 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 May 2018 16:17:23 +0200 Subject: [PATCH 327/821] Add ISSUE_TEMPLATE.md file Closes #1146. --- .github/ISSUE_TEMPLATE.md | 32 ++++++++++++++++++++++++++++++++ CHANGES.rst | 4 +++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..9a2f3b6f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,32 @@ +**This issue tracker is a tool to address bugs in Eve itself. +Please use Stack Overflow for general questions about using Eve or issues not +related to Eve (see http://python-eve.org/support).** + +If you'd like to report a bug in Eve, fill out the template below. Provide +any any extra information that may be useful / related to your problem. +Ideally, create an [MCVE](http://stackoverflow.com/help/mcve), which helps us +understand the problem and helps check that it is not caused by something in +your code. + +--- + +### Expected Behavior + +Tell us what should happen. + +```python +Paste a minimal example that causes the problem. +``` + +### Actual Behavior + +Tell us what happens instead. + +```pytb +Paste the full traceback if there was an exception. +``` + +### Environment + +* Python version: +* Eve version: diff --git a/CHANGES.rst b/CHANGES.rst index f6edd530c..541288498 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,7 +15,8 @@ Fixed Improved ~~~~~~~~ -- Add a stale-bot to automatically close stale issues and pull requests (`#1145`_) +- Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) +- Install a bot that automatically flags and then closes stale issues and pull requests (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) - Only set the package version in ``__init__.py``. (`#1142`_) @@ -23,6 +24,7 @@ Improved .. _`#1143`: https://github.com/pyeve/eve/issues/1143 .. _`#1144`: https://github.com/pyeve/eve/issues/1144 .. _`#1145`: https://github.com/pyeve/eve/issues/1145 +.. _`#1146`: https://github.com/pyeve/eve/issues/1146 Version 0.8 ----------- From 922ed1532b32f290c5adc823c4c03af959fccd82 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 May 2018 16:19:37 +0200 Subject: [PATCH 328/821] Changelog cleanup --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 541288498..d6e01a565 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,7 @@ Fixed Improved ~~~~~~~~ - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) -- Install a bot that automatically flags and then closes stale issues and pull requests (`#1145`_) +- Install a bot that flags and closes stale issues/pull requests (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) - Only set the package version in ``__init__.py``. (`#1142`_) From a1530e5a367a29a1922f75a3ed69be3ac46e8e40 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 14 May 2018 17:26:17 +0200 Subject: [PATCH 329/821] Fix RTD documentation builds --- .readthedocs.yml | 4 ++++ CHANGES.rst | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..788bab33b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,4 @@ +python: + pip_install: true + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst index d6e01a565..72c290f67 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Fixed Improved ~~~~~~~~ +- Fix documentation builds on Read the Docs. (`#1147`_) - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Install a bot that flags and closes stale issues/pull requests (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) @@ -25,6 +26,7 @@ Improved .. _`#1144`: https://github.com/pyeve/eve/issues/1144 .. _`#1145`: https://github.com/pyeve/eve/issues/1145 .. _`#1146`: https://github.com/pyeve/eve/issues/1146 +.. _`#1147`: https://github.com/pyeve/eve/issues/1147 Version 0.8 ----------- From ce4e552d9e166a149a0b524673001ee48f77ae0d Mon Sep 17 00:00:00 2001 From: Carl George Date: Mon, 14 May 2018 11:40:04 -0500 Subject: [PATCH 330/821] use simplejson module everywhere resolves #1072 --- eve/methods/get.py | 2 +- eve/tests/auth.py | 2 +- eve/tests/io/multi_mongo.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index d68c58565..d0e946270 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -13,7 +13,7 @@ import math import copy -import json +import simplejson as json from flask import current_app as app, abort, request from werkzeug import MultiDict diff --git a/eve/tests/auth.py b/eve/tests/auth.py index f2d2ddf41..35681f0f6 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import json +import simplejson as json from bson import ObjectId diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index 318dd5c11..b1cb45286 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from datetime import datetime -import json +import simplejson as json from bson import ObjectId from pymongo import MongoClient from pymongo.errors import OperationFailure From 1153ade20d073b4080316164525ce90396892828 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 May 2018 09:28:03 +0200 Subject: [PATCH 331/821] Changelog for #1148 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 72c290f67..93d0f5c8e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Fixed Improved ~~~~~~~~ +- Use ``simplejson`` everywhere in the codebase. (`#1148`_) - Fix documentation builds on Read the Docs. (`#1147`_) - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Install a bot that flags and closes stale issues/pull requests (`#1145`_) @@ -27,6 +28,7 @@ Improved .. _`#1145`: https://github.com/pyeve/eve/issues/1145 .. _`#1146`: https://github.com/pyeve/eve/issues/1146 .. _`#1147`: https://github.com/pyeve/eve/issues/1147 +.. _`#1148`: https://github.com/pyeve/eve/issues/1148 Version 0.8 ----------- From 925ab18c5d898d65a7e1cebb386a73cac08ee086 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 May 2018 09:27:37 +0200 Subject: [PATCH 332/821] Carl George --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ab7f12e52..47e208789 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,6 +24,7 @@ Patches and Contributions - Brian Mego - Bryan Cattle - Carl George +- Carl George - Carles Bruguera - Christian Henke - Christoph Witzany From 5a14b592bde36c995c5a81620706bedf704d4397 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 May 2018 09:30:53 +0200 Subject: [PATCH 333/821] Remove duplicate author --- AUTHORS | 1 - 1 file changed, 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 47e208789..ab7f12e52 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,7 +24,6 @@ Patches and Contributions - Brian Mego - Bryan Cattle - Carl George -- Carl George - Carles Bruguera - Christian Henke - Christoph Witzany From b5543d1845cb7b850742d4b384885766fc4d5a6f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 May 2018 16:32:23 +0200 Subject: [PATCH 334/821] Only display the version number on the homepage Closes #1151 --- CHANGES.rst | 2 ++ docs/conf.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 93d0f5c8e..774bb187c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- Only display the version number on the docs homepage (`#1151`_) - Replace the broken ``make audit`` shortcut with ``make check``, then add the command to ``CONTRIBUTING.rst`` where it is missing. (`#1144`_) @@ -29,6 +30,7 @@ Improved .. _`#1146`: https://github.com/pyeve/eve/issues/1146 .. _`#1147`: https://github.com/pyeve/eve/issues/1147 .. _`#1148`: https://github.com/pyeve/eve/issues/1148 +.. _`#1151`: https://github.com/pyeve/eve/issues/1151 Version 0.8 ----------- diff --git a/docs/conf.py b/docs/conf.py index 0fedb3e79..45a317d26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,7 @@ # The full version, including alpha/beta/rc tags. release = __import__('eve').__version__ # The short X.Y version. -version = release.split('-dev')[0] +version = release.split('.dev')[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 4bd442596cf0591471730ee016f5c3b62479ce8d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 16 May 2018 10:03:39 +0200 Subject: [PATCH 335/821] Update obsolete PyPI link on sidebar Closes #1152 --- CHANGES.rst | 10 ++++++---- docs/_templates/sidebarintro.html | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 774bb187c..f718b7370 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,16 +10,17 @@ Unreleased Fixed ~~~~~ -- Only display the version number on the docs homepage (`#1151`_) -- Replace the broken ``make audit`` shortcut with ``make check``, then add the - command to ``CONTRIBUTING.rst`` where it is missing. (`#1144`_) +- Replace the broken ``make audit`` shortcut with ``make check``, add the + command to ``CONTRIBUTING.rst`` it was missing. (`#1144`_) Improved ~~~~~~~~ +- Update obsolete PyPI link in docs sidebar. (`#1152`_) +- Only display the version number on the docs homepage. (`#1151`_) - Use ``simplejson`` everywhere in the codebase. (`#1148`_) - Fix documentation builds on Read the Docs. (`#1147`_) - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) -- Install a bot that flags and closes stale issues/pull requests (`#1145`_) +- Install a bot that flags and closes stale issues/pull requests. (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) - Only set the package version in ``__init__.py``. (`#1142`_) @@ -31,6 +32,7 @@ Improved .. _`#1147`: https://github.com/pyeve/eve/issues/1147 .. _`#1148`: https://github.com/pyeve/eve/issues/1148 .. _`#1151`: https://github.com/pyeve/eve/issues/1151 +.. _`#1152`: https://github.com/pyeve/eve/issues/1152 Version 0.8 ----------- diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 9b6009cb4..0c92c5780 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -21,7 +21,7 @@

      Useful Links

    • Eve @ Stack Overflow
    • Eve @ Google Groups
    • Eve @ IRC -
    • Eve @ PyPI
    • +
    • Eve @ PyPI
    • Eve @ Nicola Iarocci
    • Issue Tracker
    From 2127b6a8638821d8f6996e8cdbaa6edc860ba64e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 16 May 2018 10:13:11 +0200 Subject: [PATCH 336/821] Fix broken link to Postman app Closes #1150 --- CHANGES.rst | 2 ++ docs/index.rst | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f718b7370..db1970ea8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Fixed Improved ~~~~~~~~ +- Fix broken link to the Postman app. (`#1150`_) - Update obsolete PyPI link in docs sidebar. (`#1152`_) - Only display the version number on the docs homepage. (`#1151`_) - Use ``simplejson`` everywhere in the codebase. (`#1148`_) @@ -33,6 +34,7 @@ Improved .. _`#1148`: https://github.com/pyeve/eve/issues/1148 .. _`#1151`: https://github.com/pyeve/eve/issues/1151 .. _`#1152`: https://github.com/pyeve/eve/issues/1152 +.. _`#1150`: https://github.com/pyeve/eve/issues/1150 Version 0.8 ----------- diff --git a/docs/index.rst b/docs/index.rst index da9d3445a..58eb0687e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -115,8 +115,7 @@ link `_. .. _`source code`: https://github.com/pyeve/eve-demo .. _`usage examples`: https://github.com/pyeve/eve-demo#readme .. _`client app`: https://github.com/pyeve/eve-demo-client -.. _Postman: https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CC0QFjAA&url=https%3A%2F%2Fchrome.google.com%2Fwebstore%2Fdetail%2Fpostman-rest-client%2Ffdmmgilgnpjigdojojpjoooidkmcomcm&ei=dPQ7UpqEBISXtAbPpIGwDg&usg=AFQjCNFL71vN61QG0LKlw7VDJvIZDprjHA&bvm=bv.52434380,d.Yms - +.. _Postman: https://www.getpostman.com .. _Flask: http://flask.pocoo.org/ .. _eve-sqlalchemy: https://github.com/RedTurtle/eve-sqlalchemy .. _MongoDB: https://mongodb.org From 57a0a478102dbeb66271470356aae293d297ba1c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 17 May 2018 18:44:09 +0200 Subject: [PATCH 337/821] Fix: serializer fails with types array in schema Closes #1112 --- CHANGES.rst | 2 + eve/methods/common.py | 183 ++++++++++++++++++------------------ eve/tests/methods/common.py | 24 +++++ 3 files changed, 119 insertions(+), 90 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index db1970ea8..7f4a1cc46 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- Serializers fails when array of types is in schema. (`#1112`_) - Replace the broken ``make audit`` shortcut with ``make check``, add the command to ``CONTRIBUTING.rst`` it was missing. (`#1144`_) @@ -35,6 +36,7 @@ Improved .. _`#1151`: https://github.com/pyeve/eve/issues/1151 .. _`#1152`: https://github.com/pyeve/eve/issues/1152 .. _`#1150`: https://github.com/pyeve/eve/issues/1150 +.. _`#1112`: https://github.com/pyeve/eve/issues/1112 Version 0.8 ----------- diff --git a/eve/methods/common.py b/eve/methods/common.py index bc11cbf88..a598e7bc0 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -396,97 +396,100 @@ def resolve_schema(schema): field_schema = schema[field] if not isinstance(field_schema, dict): field_schema = rules_set_registry.get(field_schema) - field_type = field_schema.get('type') - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - optschema = dict(field_schema, **optschema) - optschema.pop(x_of, None) - serialize(document, schema={field: optschema}) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - optschema = dict(field_schema, type=opttype) - optschema.pop(x_of_type, None) - serialize(document, schema={field: optschema}) - if config.AUTO_CREATE_LISTS and field_type == 'list': - # Convert single values to lists - if not isinstance(document[field], list): - document[field] = [document[field]] - if 'schema' in field_schema: - field_schema = resolve_schema(field_schema['schema']) - if 'dict' in (field_type, field_schema.get('type')): - # either a dict or a list of dicts - embedded = [document[field]] if field_type == 'dict' \ - else document[field] - for subdocument in embedded: - if type(subdocument) is not dict: - # value is not a dict - continue serialization - # error will be reported by validation if - # appropriate - continue - elif 'schema' in field_schema: - serialize(subdocument, - schema=field_schema['schema']) - else: - serialize(subdocument, schema=field_schema) - elif field_schema.get('type') == 'list': - # a list of lists - sublist_schema = resolve_schema( - field_schema.get('schema')) - item_type = sublist_schema.get('type') - for sublist in document[field]: - for i, v in enumerate(sublist): - if item_type == 'dict': - serialize(sublist[i], - schema=sublist_schema['schema']) - elif item_type in app.data.serializers: - sublist[i] = serialize_value(item_type, v) - elif field_schema.get('type') is None: - # a list of items determined by *of rules - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - for optschema in field_schema.get(x_of, []): - serialize(document, - schema={ - field: {'type': field_type, - 'schema': optschema}}) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): - serialize( - document, - schema={field: {'type': field_type, - 'schema': {'type': - opttype}}}) - else: - # a list of one type, arbitrary length - field_type = field_schema.get('type') - if field_type in app.data.serializers: - for i, v in enumerate(document[field]): + field_types = field_schema.get('type') + if not isinstance(field_types, list): + field_types = [field_types] + for field_type in field_types: + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + optschema = dict(field_schema, **optschema) + optschema.pop(x_of, None) + serialize(document, schema={field: optschema}) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + optschema = dict(field_schema, type=opttype) + optschema.pop(x_of_type, None) + serialize(document, schema={field: optschema}) + if config.AUTO_CREATE_LISTS and field_type == 'list': + # Convert single values to lists + if not isinstance(document[field], list): + document[field] = [document[field]] + if 'schema' in field_schema: + field_schema = resolve_schema(field_schema['schema']) + if 'dict' in (field_type, field_schema.get('type')): + # either a dict or a list of dicts + embedded = [document[field]] if field_type == 'dict' \ + else document[field] + for subdocument in embedded: + if type(subdocument) is not dict: + # value is not a dict - continue serialization + # error will be reported by validation if + # appropriate + continue + elif 'schema' in field_schema: + serialize(subdocument, + schema=field_schema['schema']) + else: + serialize(subdocument, schema=field_schema) + elif field_schema.get('type') == 'list': + # a list of lists + sublist_schema = resolve_schema( + field_schema.get('schema')) + item_type = sublist_schema.get('type') + for sublist in document[field]: + for i, v in enumerate(sublist): + if item_type == 'dict': + serialize(sublist[i], + schema=sublist_schema['schema']) + elif item_type in app.data.serializers: + sublist[i] = serialize_value(item_type, v) + elif field_schema.get('type') is None: + # a list of items determined by *of rules + for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for optschema in field_schema.get(x_of, []): + serialize(document, + schema={ + field: {'type': field_type, + 'schema': optschema}}) + x_of_type = '{0}_type'.format(x_of) + for opttype in field_schema.get(x_of_type, []): + serialize( + document, + schema={field: {'type': field_type, + 'schema': {'type': + opttype}}}) + else: + # a list of one type, arbitrary length + field_type = field_schema.get('type') + if field_type in app.data.serializers: + for i, v in enumerate(document[field]): + document[field][i] = \ + serialize_value(field_type, v) + elif 'items' in field_schema: + # a list of multiple types, fixed length + for i, (s, v) in enumerate(zip(field_schema['items'], + document[field])): + field_type = s.get('type') + if field_type in app.data.serializers: document[field][i] = \ - serialize_value(field_type, v) - elif 'items' in field_schema: - # a list of multiple types, fixed length - for i, (s, v) in enumerate(zip(field_schema['items'], - document[field])): - field_type = s.get('type') - if field_type in app.data.serializers: - document[field][i] = \ - serialize_value(field_type, document[field][i]) - elif 'valueschema' in field_schema: - # a valueschema - field_type = field_schema['valueschema']['type'] - if field_type == 'objectid': - target = document[field] - for field in target: - target[field] = \ - serialize_value(field_type, target[field]) - elif field_type == 'dict': - for subdocument in document[field].values(): - serialize( - subdocument, - schema=field_schema['valueschema']['schema']) - elif field_type in app.data.serializers: - # a simple field - document[field] = \ - serialize_value(field_type, document[field]) + serialize_value(field_type, document[field][i]) + elif 'valueschema' in field_schema: + # a valueschema + field_type = field_schema['valueschema']['type'] + if field_type == 'objectid': + target = document[field] + for field in target: + target[field] = \ + serialize_value(field_type, target[field]) + elif field_type == 'dict': + for subdocument in document[field].values(): + serialize( + subdocument, + schema=field_schema['valueschema']['schema']) + elif field_type in app.data.serializers: + # a simple field + document[field] = \ + serialize_value(field_type, document[field]) return document diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 25cb8daca..4ce748d23 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -15,6 +15,30 @@ class TestSerializer(TestBase): + def test_serialize_array_of_tipes(self): + # see #1112. + schema = { + 'val': { + 'type': 'dict', + 'schema': { + 'x': {'type': ['string', 'number']}, + 'timestamp': {'type': 'datetime'} + } + } + } + + doc = {'val':{'x': '1', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertEqual(serialized['val']['x'], 1) + self.assertTrue(isinstance(serialized['val']['timestamp'], datetime)) + + doc = {'val':{'x': 's', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + with self.app.app_context(): + serialized = serialize(doc, schema=schema) + self.assertEqual(serialized['val']['x'], 's') + self.assertTrue(isinstance(serialized['val']['timestamp'], datetime)) + def test_serialize_subdocument(self): # tests fix for #244, serialization of sub-documents. schema = {'personal': {'type': 'dict', From 39a685d9c73954b9329033d109d25e821f7fe27d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 18 May 2018 10:38:41 +0200 Subject: [PATCH 338/821] flake8 --- eve/methods/common.py | 41 ++++++++++++++++++++++--------------- eve/tests/methods/common.py | 4 ++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index a598e7bc0..d80919b80 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -418,17 +418,17 @@ def resolve_schema(schema): field_schema = resolve_schema(field_schema['schema']) if 'dict' in (field_type, field_schema.get('type')): # either a dict or a list of dicts - embedded = [document[field]] if field_type == 'dict' \ - else document[field] + embedded = [document[field]] \ + if field_type == 'dict' else document[field] for subdocument in embedded: if type(subdocument) is not dict: - # value is not a dict - continue serialization - # error will be reported by validation if - # appropriate + # value is not a dict - continue + # serialization error will be reported by + # validation if appropriate continue elif 'schema' in field_schema: serialize(subdocument, - schema=field_schema['schema']) + schema=field_schema['schema']) else: serialize(subdocument, schema=field_schema) elif field_schema.get('type') == 'list': @@ -439,25 +439,29 @@ def resolve_schema(schema): for sublist in document[field]: for i, v in enumerate(sublist): if item_type == 'dict': - serialize(sublist[i], - schema=sublist_schema['schema']) + serialize( + sublist[i], + schema=sublist_schema['schema']) elif item_type in app.data.serializers: - sublist[i] = serialize_value(item_type, v) + sublist[i] = serialize_value( + item_type, v) elif field_schema.get('type') is None: # a list of items determined by *of rules for x_of in ['allof', 'anyof', 'oneof', 'noneof']: for optschema in field_schema.get(x_of, []): serialize(document, - schema={ - field: {'type': field_type, - 'schema': optschema}}) + schema={ + field: { + 'type': field_type, + 'schema': optschema}}) x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get(x_of_type, []): + for opttype in field_schema.get( + x_of_type, []): serialize( document, schema={field: {'type': field_type, 'schema': {'type': - opttype}}}) + opttype}}}) else: # a list of one type, arbitrary length field_type = field_schema.get('type') @@ -468,11 +472,12 @@ def resolve_schema(schema): elif 'items' in field_schema: # a list of multiple types, fixed length for i, (s, v) in enumerate(zip(field_schema['items'], - document[field])): + document[field])): field_type = s.get('type') if field_type in app.data.serializers: document[field][i] = \ - serialize_value(field_type, document[field][i]) + serialize_value(field_type, + document[field][i]) elif 'valueschema' in field_schema: # a valueschema field_type = field_schema['valueschema']['type'] @@ -485,7 +490,9 @@ def resolve_schema(schema): for subdocument in document[field].values(): serialize( subdocument, - schema=field_schema['valueschema']['schema']) + schema=field_schema + ['valueschema']['schema']) + elif field_type in app.data.serializers: # a simple field document[field] = \ diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 4ce748d23..cf799fb78 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -27,13 +27,13 @@ def test_serialize_array_of_tipes(self): } } - doc = {'val':{'x': '1', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + doc = {'val': {'x': '1', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} with self.app.app_context(): serialized = serialize(doc, schema=schema) self.assertEqual(serialized['val']['x'], 1) self.assertTrue(isinstance(serialized['val']['timestamp'], datetime)) - doc = {'val':{'x': 's', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + doc = {'val': {'x': 's', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} with self.app.app_context(): serialized = serialize(doc, schema=schema) self.assertEqual(serialized['val']['x'], 's') From e4568008d1f34bf94d3a56409fa657a0de45be4c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 28 May 2018 10:24:51 +0200 Subject: [PATCH 339/821] Fix SchemaError when VALIDATE_FILTERS = True Closes #1154. --- CHANGES.rst | 2 ++ docs/config.rst | 9 +++++++++ eve/tests/utils.py | 15 +++++++++++++++ eve/utils.py | 5 ++--- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7f4a1cc46..e061855cb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- ``cerberus.schema.SchemaError`` when ``VALIDATE_FILTERS = True``. (`#1154`_) - Serializers fails when array of types is in schema. (`#1112`_) - Replace the broken ``make audit`` shortcut with ``make check``, add the command to ``CONTRIBUTING.rst`` it was missing. (`#1144`_) @@ -37,6 +38,7 @@ Improved .. _`#1152`: https://github.com/pyeve/eve/issues/1152 .. _`#1150`: https://github.com/pyeve/eve/issues/1150 .. _`#1112`: https://github.com/pyeve/eve/issues/1112 +.. _`#1154`: https://github.com/pyeve/eve/issues/1154 Version 0.8 ----------- diff --git a/docs/config.rst b/docs/config.rst index 77f4e5ca3..8be9dfa43 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -140,6 +140,15 @@ uppercase. resource schema. Invalid filters will throw an exception. Defaults to ``False``. + Word of caution: validation on filter + expressions involving fields with custom + rules or types might have a considerable + impact on performance. This is the case, + for example, with ``data_relation``-rule + fields. Consider excluding heavy-duty + fields from filters (see + ``ALLOWED_FILTERS``). + ``SORTING`` ``True`` if sorting is supported for ``GET`` requests, otherwise ``False``. Can be overridden by resource settings. Defaults diff --git a/eve/tests/utils.py b/eve/tests/utils.py index 4025bf05d..72401ca89 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -220,6 +220,21 @@ def test_debug_error_message(self): self.assertEqual(debug_error_message('An error message'), 'An error message') + def test_validate_filters_when_custom_types_are_used(self): + # Filters validation should operate on the active validator instance, + # not on Cerberus' standard one. See #1154. + self.app.config['VALIDATE_FILTERS'] = True + response, status = self.get(self.known_resource, + query='?where={"tid":"1234"}') + self.assert400(status) + self.assertTrue("filter on 'tid' is invalid" in + response['_error']['message']) + + response, status = self.get( + self.known_resource, + query='?where={"tid":"5a1154523a6bcc1d245e143d"}') + self.assert200(status) + def test_validate_filters(self): self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = [] with self.app.test_request_context(): diff --git a/eve/utils.py b/eve/utils.py index 215f808f7..d469b0e03 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -16,7 +16,6 @@ import eve import hashlib import werkzeug.exceptions -from cerberus import Validator from copy import copy from flask import request from flask import current_app as app @@ -454,12 +453,12 @@ def recursive_validate_filter(key, value, schema): return False else: field_schema = schema.get(key) - v = Validator({key: field_schema}) + v = app.validator({key: field_schema}) return v.validate({key: value}) res_schema = config.DOMAIN[resource]['schema'] if not recursive_validate_filter(key, value, res_schema): - return "filter on '%s' is invalid" + return "filter on '%s' is invalid" % key return None From ee0b8ceeb0fa0e63ec06ba73e8e4a1c5d1f91c7f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 29 May 2018 16:53:55 +0200 Subject: [PATCH 340/821] Port docs configuration file to Python 3 Also, reformat it to match Black coding style. --- docs/conf.py | 205 ++++++++++++++++++++++++++------------------------- 1 file changed, 106 insertions(+), 99 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 45a317d26..41ece9353 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,109 +17,116 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath('.')) -sys.path.append(os.path.abspath('..')) -sys.path.append(os.path.abspath('_themes')) +sys.path.append(os.path.abspath(".")) +sys.path.append(os.path.abspath("..")) +sys.path.append(os.path.abspath("_themes")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'alabaster', - 'sphinxcontrib.embedly'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "alabaster", + "sphinxcontrib.embedly", +] # sphinxcontrib.embedly -embedly_key = '76207aa23dde489bba6bcbc9e56193a6' +embedly_key = "76207aa23dde489bba6bcbc9e56193a6" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Eve' -copyright = u'%s. Python-Eve is a Nicola Iarocci Project' % datetime.datetime.now().year +project = u"Eve" +copyright = ( + u'%s. Python-Eve is a Nicola Iarocci Project' + % datetime.datetime.now().year +) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. -release = __import__('eve').__version__ +release = __import__("eve").__version__ # The short X.Y version. -version = release.split('.dev')[0] +version = release.split(".dev")[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -#pygments_style = 'sphinx' +# pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'default' -#html_theme = 'flask' -html_theme = 'alabaster' +# html_theme = 'default' +# html_theme = 'flask' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {'touch_icon': 'touch-icon.png'} +# html_theme_options = {'touch_icon': 'touch-icon.png'} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [alabaster.get_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = "favicon.png" +# html_logo = "favicon.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -129,128 +136,122 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} -#html_sidebars = { +# html_sidebars = {} +# html_sidebars = { # 'index': ['sidebarintro.html', 'searchbox.html', 'sidebarfooter.html'], # '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', # 'sourcelink.html', 'searchbox.html'] -#} +# } html_sidebars = { - '**': [ - 'about.html', - 'sidebarintro.html', - 'navigation.html', - 'searchbox.html', - 'artwork.html', - ] + "**": [ + "about.html", + "sidebarintro.html", + "navigation.html", + "searchbox.html", + "artwork.html", + ] } html_theme_options = { - 'logo': 'eve_leaf.png', - 'github_user': 'pyeve', - 'github_repo': 'eve', - 'github_type': 'star', - 'github_banner': 'forkme_right_green_007200.png', - 'show_powered_by': False, + "logo": "eve_leaf.png", + "github_user": "pyeve", + "github_repo": "eve", + "github_type": "star", + "github_banner": "forkme_right_green_007200.png", + "show_powered_by": False, } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False -#html_use_modindex = False +# html_use_modindex = False # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'Evedoc' +htmlhelp_basename = "Evedoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Eve.tex', u'Eve Documentation', - u'Nicola Iarocci', 'manual'), + ("index", "Eve.tex", u"Eve Documentation", u"Nicola Iarocci", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'eve', u'Eve Documentation', - [u'Nicola Iarocci'], 1) -] +man_pages = [("index", "eve", u"Eve Documentation", [u"Nicola Iarocci"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -259,38 +260,44 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Eve', u'Eve Documentation', - u'Nicola Iarocci', 'Eve', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "Eve", + u"Eve Documentation", + u"Nicola Iarocci", + "Eve", + "One line description of project.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'http://docs.python.org/': None} -intersphinx_mapping = {'cerberus': ('http://docs.python-cerberus.org/en/latest/', None)} +# intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {"cerberus": ("http://docs.python-cerberus.org/en/latest/", None)} -pygments_style = 'flask_theme_support.FlaskyStyle' +pygments_style = "flask_theme_support.FlaskyStyle" # fall back if theme is not there try: - __import__('flask_theme_support') -except ImportError, e: - print '-' * 74 - print 'Warning: Flask themes unavailable. Building with default theme' - print 'If you want the Flask themes, run this command and build again:' + __import__("flask_theme_support") +except ImportError as e: + print("-" * 74) + print("Warning: Flask themes unavailable. Building with default theme") + print("If you want the Flask themes, run this command and build again:") print - print ' git submodule update --init' - print '-' * 74 + print(" git submodule update --init") + print("-" * 74) - pygments_style = 'tango' - html_theme = 'default' + pygments_style = "tango" + html_theme = "default" html_theme_options = {} From 6398aba5034ca1d2d7c065108e92365cc5fe00e4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 29 May 2018 17:04:17 +0200 Subject: [PATCH 341/821] Reformat code to match Black code-style Closes #1155. --- CHANGES.rst | 12 +- CONTRIBUTING.rst | 12 +- Makefile | 2 +- README.rst | 20 +- artwork/LICENSE | 1 - artwork/logo.ai | 208 +-- docs/_templates/artwork.html | 1 - docs/_themes/flask/static/flasky.css_t | 48 +- docs/_themes/flask_small/static/flasky.css_t | 44 +- docs/_themes/flask_theme_support.py | 147 +- docs/authentication.rst | 38 +- docs/changelog.rst | 2 - docs/extensions.rst | 10 +- docs/features.rst | 10 +- docs/foreword.rst | 4 +- docs/funding.rst | 2 +- docs/index.rst | 9 +- docs/quickstart.rst | 28 +- docs/rest_api_for_humans.rst | 8 +- docs/snippets/hooks_blueprints.rst | 2 +- docs/snippets/template.rst | 2 +- docs/support.rst | 4 +- docs/tutorials/account_management.rst | 48 +- docs/tutorials/custom_idfields.rst | 10 +- docs/tutorials/index.rst | 2 +- docs/updates.rst | 4 +- docs/validation.rst | 14 +- eve/__init__.py | 50 +- eve/auth.py | 141 +- eve/default_settings.py | 158 +- eve/endpoints.py | 101 +- eve/exceptions.py | 2 + eve/flaskapp.py | 668 ++++---- eve/io/base.py | 92 +- eve/io/mongo/flask_pymongo.py | 85 +- eve/io/mongo/geo.py | 44 +- eve/io/mongo/media.py | 12 +- eve/io/mongo/mongo.py | 309 ++-- eve/io/mongo/parser.py | 40 +- eve/io/mongo/validation.py | 97 +- eve/logging.py | 2 + eve/methods/common.py | 507 +++--- eve/methods/delete.py | 78 +- eve/methods/get.py | 250 +-- eve/methods/patch.py | 68 +- eve/methods/post.py | 77 +- eve/methods/put.py | 88 +- eve/render.py | 150 +- eve/tests/__init__.py | 429 ++--- eve/tests/auth.py | 621 ++++--- eve/tests/config.py | 576 +++---- eve/tests/endpoints.py | 228 ++- eve/tests/io/flask_pymongo.py | 60 +- eve/tests/io/media.py | 288 ++-- eve/tests/io/mongo.py | 471 ++--- eve/tests/io/multi_mongo.py | 177 +- eve/tests/logging.py | 6 +- eve/tests/methods/common.py | 835 +++++---- eve/tests/methods/delete.py | 347 ++-- eve/tests/methods/get.py | 1612 +++++++++--------- eve/tests/methods/patch.py | 561 +++--- eve/tests/methods/post.py | 740 ++++---- eve/tests/methods/put.py | 395 +++-- eve/tests/methods/ratelimit.py | 30 +- eve/tests/renders.py | 378 ++-- eve/tests/response.py | 73 +- eve/tests/test_prefix.py | 6 +- eve/tests/test_prefix_version.py | 6 +- eve/tests/test_settings.py | 489 +++--- eve/tests/test_settings_env.py | 2 +- eve/tests/test_version.py | 4 +- eve/tests/utils.py | 332 ++-- eve/tests/versioning.py | 751 ++++---- eve/utils.py | 176 +- eve/validation.py | 48 +- eve/versioning.py | 160 +- examples/notifications.py | 7 +- examples/notifications_settings.py | 5 +- examples/security/bcrypt.py | 12 +- examples/security/hmac.py | 18 +- examples/security/roles.py | 10 +- examples/security/settings_security.py | 44 +- examples/security/sha1-hmac.py | 9 +- examples/security/token.py | 8 +- setup.py | 82 +- 85 files changed, 7055 insertions(+), 6652 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e061855cb..3ab5a6c33 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -80,7 +80,7 @@ Released on May 10, 2018. - New: Add support for MongoDB ``$caseSensitive`` and ``$diactricSensitive`` query operators (`#1126`_). - New: Add support for MongoDB bitwise query operators ``$bitsAllClear``, - ``$bitsAllSet``, ``$bitsAnyClear``, ``$bitsAnySet`` (`#1053`_). + ``$bitsAllSet``, ``$bitsAnyClear``, ``$bitsAnySet`` (`#1053`_). - New: support for ``MONGO_AUTH_MECHANISM`` and ``MONGO_AUTH_MECHANISM_PROPERTIES``. - New: ``MONGO_DBNAME`` can now be used in conjuction with ``MONGO_URI``. @@ -124,14 +124,14 @@ Released on May 10, 2018. - PyMongo dependency set to >=3.5. - Events dependency set to >=v0.3. - Drop Flask-PyMongo dependency, use custom code instead (`#855`_). -- Docs: Comprehensive rewrite of the `How to contribute`_ page. +- Docs: Comprehensive rewrite of the `How to contribute`_ page. - Docs: Drop the testing page; merge its contents with `How to contribute`_. - Docs: Add link to the `Eve course`_. It was authored by the project author, and it is hosted by TalkPython Training. - Docs: code snippets are now Python 3 compatibile (Pahaz Blinov). - Dev: Delete and cleanup of some unnecessary code. - Dev: after the latest update (May 4th) travis-ci would not run tests on - Python 2.6. + Python 2.6. - Dev: all branches are now tested on travis-ci. Previously, only 'master' was being tested. - Dev: fix insidious bug in ``tests.methods.post.TestPost`` class. @@ -144,7 +144,7 @@ Breaking Changes - Eve now relies on `Cerberus`_ 1.1+ (`#776`_). It allows for many new powerful validation and trasformation features (like `schema registries`_), improved performance and, in general, a more streamlined API. It also brings - some notable breaking changes. + some notable breaking changes. - ``keyschema`` was renamed to ``valueschema``, and ``propertyschema`` to ``keyschema``. @@ -327,7 +327,7 @@ Released on 6 February, 2017 this feature on can greatly improve performance. Defaults to ``False`` (slower performance; document count included; accurate ``HATEOAS``). Closes #944 and #853. - + - New: ``Location`` header is returned on ``201 Created`` POST responses. If will contain the URI to the created document. If bulk inserts are enabled, @@ -426,7 +426,7 @@ Released on 6 February, 2017 - Fix: Versioning does not work with User Restricted Resource Access. Closes #967 (Kris Lambrechts) -- Fix: ``test_create_indexes()`` typo. Closes 960. +- Fix: ``test_create_indexes()`` typo. Closes 960. - Fix: fix crash when attempting to modify a document ``_id`` on MongoDB 3.4 (Giorgos Margaritis) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index dee0228f3..2577ffb91 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -4,7 +4,7 @@ How to contribute Contributions are welcome! Not familiar with the codebase yet? No problem! There are many ways to contribute to open source projects: reporting bugs, helping with the documentation, spreading the word and of course, adding -new features and patches. +new features and patches. Support questions ----------------- @@ -13,7 +13,7 @@ Please, don't use the issue tracker for this. Use one of the following resources for questions about your own code: * Ask on `Stack Overflow`_. Search with Google first using: ``site:stackoverflow.com eve {search term, exception message, etc.}`` -* The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting feedback. +* The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting feedback. * The IRC channel ``#python-eve`` on FreeNode. .. _Stack Overflow: https://stackoverflow.com/questions/tagged/eve?sort=linked @@ -149,14 +149,14 @@ Rate limiting tests While there are no test requirements for most of the suite, please be advised that in order to execute the :ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting tests are silently skipped if any of the two -conditions are not met. +conditions are not met. Building the docs ~~~~~~~~~~~~~~~~~ Build the docs in the ``docs`` directory using Sphinx:: cd docs - make html + make html Open ``_build/html/index.html`` in your browser to view the docs. @@ -177,7 +177,7 @@ First time contributor? ----------------------- It's alright. We've all been there. See next chapter. -Don't know where to start? +Don't know where to start? -------------------------- There are usually several TODO comments scattered around the codebase, maybe check them out and see if you have ideas, or can help with them. Also, check @@ -201,5 +201,3 @@ Guide to Pull Requests`_ .. _`Pull Request`: https://help.github.com/articles/creating-a-pull-request .. _`running the tests`: http://python-eve.org/testing#running-the-tests .. _Redis: https://redis.io - - diff --git a/Makefile b/Makefile index 7e793cb51..8578316d9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload +.PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload install-dev: pip install -q -e .[dev] diff --git a/README.rst b/README.rst index ee64e2750..07d6f6a82 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,19 @@ Eve ==== -.. image:: https://secure.travis-ci.org/pyeve/eve.svg?branch=master - :target: https://secure.travis-ci.org/pyeve/eve +.. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square + :target: https://pypi.org/project/eve + +.. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square + :target: https://travis-ci.org/pyeve/eve + +.. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square + :target: https://pypi.org/project/eve + +.. image:: https://img.shields.io/badge/license-BSD-blue.svg?style=flat-square + :target: https://en.wikipedia.org/wiki/BSD_License + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black Eve is an open source Python REST API framework designed for human beings. It allows to effortlessly build and deploy highly customizable, fully featured @@ -75,7 +87,7 @@ a business and are using Eve in a revenue-generating product, it would make business sense to sponsor Eve development: it ensures the project that your product relies on stays healthy and actively maintained. Individual users are also welcome to make a recurring pledge or a one time donation if Eve has -helped you in your work or personal projects. +helped you in your work or personal projects. Every single sign-up makes a significant impact towards making Eve possible. To learn more, check out our `funding page`_. @@ -84,7 +96,7 @@ License ------- Eve is a `Nicola Iarocci`_ open source project, distributed under the `BSD license -`_. +`_. .. _`Nicola Iarocci`: http://nicolaiarocci.com .. _`funding page`: http://python-eve.org/funding diff --git a/artwork/LICENSE b/artwork/LICENSE index ac22ecd6e..1963da290 100644 --- a/artwork/LICENSE +++ b/artwork/LICENSE @@ -299,4 +299,3 @@ WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. - diff --git a/artwork/logo.ai b/artwork/logo.ai index 81049a552..c2a4bf83b 100644 --- a/artwork/logo.ai +++ b/artwork/logo.ai @@ -643,27 +643,27 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 8 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/Thumb 16 0 R/TrimBox[5.66899 5.66901 685.979 685.979]/Type/Page>> endobj 54 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/TrimBox[5.66901 5.66901 685.979 685.979]/Type/Page>> endobj 109 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/TrimBox[5.66899 5.66895 685.979 685.979]/Type/Page>> endobj 110 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/TrimBox[5.66901 5.66895 685.979 685.979]/Type/Page>> endobj 308 0 obj <>stream Htю7E+DeGv P vAKDV画ZaHm}pzs۰np(kHCx^C m$i.Czk;/g#&栣p^OsRE5e祋ĸȨI XR~԰&>/ʄ6'UD&2HochE!)2\d3C\Pl?JZT|Zc"Ii^d`R=ҥ@Ρ?K6`<€J1B2F!z E2B9`i8ƐT\XZ3FڞXiUp @@ -695,7 +695,7 @@ H 7 ˰\\y\S۬T|j &hvp /S)zcZz9'7Zh[SlJKK5BkA^n93=ͨOR8x)h|wHrNEFDESp*/ZBqvv #h'Y&|mmGeUuMm^}ښJ56kAnNVFQNO;Ƨɸ[;esg&H)rOu7/RZDySya,/Ж?2tQ( :Ya[u5u66lkrs6w9[i޸f^Ddg:m\4aݗ)&#{.F"YB U!mD|yr!WOz;/\H*Mg-*)wT768;\[۽/]=ۻt:-͍ uU9#UViGq 9 &"KbȪ -j5=h"jt܈#t2ݝM70RLD6gb%k*ETNzTnKd ,K|mU݆vWW} |0xhᑑy##G `;\͍k*+JX3IJY$:R(~dXKdH<#QV;nhzyJY*EKZєE {ۻKR30IO)+*kZ;:wڳ##ǎ8usc;~3O=~lzumvnj\_(+.H>C]jOa'OJ6s +j5=h"jt܈#t2ݝM70RLD6gb%k*ETNzTnKd ,K|mU݆vWW} |0xhᑑy##G `;\͍k*+JX3IJY$:R(~dXKdH<#QV;nhzyJY*EKZєE {ۻKR30IO)+*kZ;:wڳ##ǎ8usc;~3O=~lzumvnj\_(+.H>C]jOa'OJ6s DtZ&6QFޞlwDdXTWXkN7&'Xo{[]@.Sqc\*2:Ř_hXWaSۻ[{wphg.\+W]_vg^0vяFP(/ 1Q|2"˦ ?NET֙6{u54ճQ^b6Պ8^WRXg)zSvԹYn7tjT,byKmzSkLK]]\^IwnЇ؅K^q֗wwGMNNχΗn_z҅3'G=Dlu9)ҢTZ)$͕9Q$}Z~66Hlij,+2jEa!^uWG/Ka"l1+-vMזX-$YlЫc;@p\d!vWi&s:sۮ8s7n޾{'~3Ocjꇧ͓_O>|p7]SǎzO?ŋ1=/?㳩>y<9q/S6gN|oOϖ-R[%#fHH v-"HD1f[˪];vp09:t{elܗ{4Yn$Bm$H>tdDZ%tlI4Jrɥr]OBh1Ƹ4c107״9 >Z͛8qXc檼Gw9Bdr梽p$@ܑ)gg&FdD?sdOW$Q&!~yTZD-ec<>9|/I@gy"*sێ!`a1nbֆo t{(~;&Gǡ3 l+/NyzҹS($"yvpPsp],Oߏ d;1P[]uZD l#odFUumC+D(}c7MY]{ 8&)YL-aI,X~ݬȩsl]nr㉽!<*b5qZL676TWUbEUYU]S[܊$)}4㏈S1$3*beGq!ˤ-+JyXWK}ǖ ?`9 nӎCmnE$W4ilćflf'ׂ>Ţ-wC6hO!/0!`ӈM~7lOڱi4!rPV2 l2yM$tQy}wp&ёaR'WW-f33SS`%'gds +^`J˱յ -xGg rDh>q!ˤ-+JyXWK}ǖ ?`9 nӎCmnE$W4ilćflf'ׂ>Ţ-wC6hO!/0!`ӈM~7lOڱi4!rPV2 l2yM$tQy}wp&ёaR'WW-f33SS`%'gds 1e:`C,O oN[o>}hewέ/>ɥW+Wฬ7 H2yE mwK嫴?.ة^QA~$=al Uvj{@tګʖnE~!MV\mZ{W^|CklcQ5tp{45>`G<@?Dhk3ҒDE`#b'&esJJMmb`q?43![Oh?CݿuR˶w1uZm&9$ tTB=e|cm=+;v;$%@N{NjFw59s B,IXΜrKT7wRCst* \_]y @@ -725,7 +725,7 @@ LvJ| 4W@DlrfޝQ b^Mu=.leT"Iygp!)twX|F~Ec0ggf}^Yzb0 ?>m%۰h зf8YX3g>/ w5Vgćy;YPsp4D(1GfIi`[R=̙[?eOukWse!!;YC,%K)+kPhDR֊VZvl)jM =w<9Gw}?u?dYʺ[jK2o^ ;u.";+#Rv(|rAl2NRf6bn-(k"I7c an&Z T:2 {=R]u+:㸅2$R5pIV?`vͬu383uOn^9fs@]ZP|-N7-G1Jxq[YE5-dyZWdc*+.ƌlF42~aM#fiჳg03!{=R]u+:㸅2$R5pIV?`vͬu383uOn^9fs@]ZP|-N7-G1Jxq[YE5-dyZWdc*+.ƌlF42~aM#fiჳg03!cSG%?(98֚' T!Dg^U+g{OjZǦg~79ArcgB z&pjٸHtn~$+Egk$ ?'8h+V/cd&pE*iE'{T9JVh(Ms @@ -808,7 +808,7 @@ Eږ͛ aiSK_6lg䙚1]kp|f~C`jJr3{?h,+." ѐ#2!AJK4 ^DRNYKIˁa1əe]+GgI>gmɊ he"m5G$,mhZk^&F\?p@MVDM-Z`y噖ù+cRk23 f4/39n%7' #uiQA^D{ C% L;#3+"klqū"S_)k#čuP?㖆#(RdDq -H(j9s9״Ʈɹ/(pWcE^گ!1VD1"-mj  Љ `WK)k]f +/x{/`v)< BCb72_(@ $0c.>A)w;glh&0ڒ짱`Ks:hSQaG<y#,ȤWum+Mv[]-w'V /ϸ +MU숳gG[{Wn,|hDOSYNRDbabpU& `B ; +H(j9s9״Ʈɹ/(pWcE^گ!1VD1"-mj  Љ `WK)k]f +/x{/`v)< BCb72_(@ $0c.>A)w;glh&0ڒ짱`Ks:hSQaG<y#,ȤWum+Mv[]-w'V /ϸ +MU숳gG[{Wn,|hDOSYNRDbabpU& `B ; DݦV'\݊}WRwhIXf'{ZJso\[6a^,+Ȳ#ȍCˈK(u_=<5?~@<75X{娙H%\ 1Мbr^ | mihYۼ"VN_Ե.K{av|urTE'+C Y%\ g.{72 *F,L u4֔L @@ -829,7 +829,7 @@ u ;wZҊNr0&Ӓ  ,/(οey''\cي)/y{bR,w/W0BX0P-Ŷ9+ *k+ o_Immh$GeYy  lv}E L ' llԇ(=Ơ vw몞<UNr3K ,-m3{O􌬌ĘpO{3i+b+9?A5)Y0Ttl}w߁Kp%;ꆦ֎n2:Lg.>.cnnnLMA10PZ+~)˾Uƒx:o65D]V dس,Ŷ)#8XF>VrA&z}c-;ݼ?~’5 MNR/:D8_ 0B -659)O>aFajvBSCMӒ»$B}{wn57׆+2pc[' ,2&6.6&2,Do6Nrm5Ҡn +659)O>aFajvBSCMӒ»$B}{wn57׆+2pc[' ,2&6.6&2,Do6Nrm5Ҡn /@ qq6kl1B%uu6 xeg\v葰CnvZ*k[q#d-WTV542>}6rVNGek/ m=}C0хb2:mxhpL""T?yTt?/'rGCv&`)+JBXؖWRhda F5%y8~^F4m=(sHDKMʹ}Aqɓ_+_㛚~EYw pg;00ܙa ( `B @@ -858,7 +858,7 @@ U .Jl6mHju,bU6+s hܸd-ʥ}wi-sun=0Ľi-_*)U_ˈb$na+;ϧT;ppA7C4.*Iߥa8Mm.ACi7\j|fiԫ)]ޭjʄU]3(í whJch-4x7h׿*P0H됎L랇ڡuÂ,{Bz}8vggҲd[!XTZZ.vlAg {;Sm`vؿ`~?ga. 3Ì{L^WYe4]L7ok!wI~Ira^=C#Zh`Wu}p)"z7ff&3$FJ8Ҷ5m @@ -919,7 +919,7 @@ W> l/i^3;iڐ0sĀZnS qW7Np:([568ViAFޜ~h9Pldüj2dO +61--1Ewv =JCHW34܏&x8,&#Rc3Dvz6RSyu_N/nmكvT֥Y˼?RFװKzn9Q4gC^5l`P\ܲG&ޫ` 9PҞٲXr6 -V4,{a؄\tcY`]lǿԾar鴯؏=b!&Yb ^[\aYt$w +V4,{a؄\tcY`]lǿԾar鴯؏=b!&Yb ^[\aYt$w [R)i[{$7f"o Xp zBz'hO|Ō4ǐ|-j :}̴a%Tv5Y9QK d0 ?$ćH|#uD3 phrd@,@XmVKY@ou([8#!OM~.7SoJn%|zC E T)f/:X1}J+>_~Q;^ㆪvs&۸>.k7yZS:˩㜍rݖۜaKa!l.g57Kv0!;ڗfe %]"XT J3aժlwVj=v姠αe=bI/gH& :g,(y 27>aba88fVVqɌT0NɉB`( _"fo! t}Wg_0}HX 9,Qx=~Jٹx>ӱe9M2mFS)Vk-eZFF٥btg0O?Dǐ%7eyښ6WSCyeUS}l`a8i g"1лJ"|PKڝc,$+&PvꖴGBoj_t4I vqf熚(eC!b׼^SbYi1¨;2W`/7uh?4 -!z@#(T 6 ^!R S#>E/Sq9z_ /G%ӈ0C9[ۼ@(٩P ,}XTOkpQȫUG6 x2e,> -?ϭQެYz/T5FL^`tީ3\#̬D:,vw[mDW)TBZ`0Ֆ`3tBQ˟kks41y `\޸cV#z`XHhwA0چFTyqӵܫ*F˪%*/>9 +!z@#(T 6 ^!R S#>E/Sq9z_ /G%ӈ0C9[ۼ@(٩P ,}XTOkpQȫUG6 x2e,> -?ϭQެYz/T5FL^`tީ3\#̬D:,vw[mDW)TBZ`0Ֆ`3tBQ˟kks41y `\޸cV#z`XHhwA0چFTyqӵܫ*F˪%*/>9 gS'"b'zL=N)cs*bR)W<#S 癛)K &L\9WtW!Y17i*%wJ_ 閥nWJ!p-0T`:K6B+SzlL,~J#ZLHBEe߈Eq1 ڸTD}bB;*OTCnՍl$OYQ0mz7o9NŻ|hDV[Ve֩b7YZÖHl~I)ܻJ5oOݑ%(,hZGҼmRd!/NEWutV57z;jjs^^lDǾ0-a_aL؁w44簍b^ppi&nX uƻ-݂ -cY4_g ?jGIfH %J҂[%ϩC6OzvWzoZtA$?z;ؼFT2/+0@@S<@>0bSuqw;j4S'/4sEթ(P[V^5ƊHkg/ۄw 0*֭ ajyB5TC J(_F4!m, RN ?S9 :״OfOV"յڇ1,V)S@._ @@ -972,7 +972,7 @@ e bk*ĉ_VTm }D51oUQu *nҹDU4|>WxA_PZ? Q y1>yK\.!OqM 0Cl];Sk)=RZ@[ɷ5JBeǐ$Ni"0 -úR4H~9.☫|Dϸah-)r~"eoMK%4 _7"‘e QD~0T.>"x*O>酧.Ey+HVy55RWsEk*PxEGB;(J X(8hiqmh^ 0`}_APWDLZ‹]<4zG֦`oyZR|u^gCF#nr)Va5ƪw9njyIt -xI1bIy>}-AگOShKFx6xqqQ +xI1bIy>}-AگOShKFx6xqqQ 3SU\ka椚̩Di~ ?{>J3mtߐZt]YNju]ɒQYlZZsNѴѷW>Sݥ0Bj+7q҄fU7m :8^;#eտ+*,_CY3MSU*LX.jQȖg_IWJ5a"9R'C\y׳qH)VU-Z.\+Ѥ/aen/|F[?SPkr" ^Y>VH9 &yaIxQfd}+] @@ -1008,7 +1008,7 @@ tE=H Wh*¥{I/1YwûJy׸jk@p[z3*ReRXwq30u%BAŒ%\NC)W'5꡵ &F+U,d5gR "JrVDBSDO]V[EסdyVӃ1,+Iev"`WrwKaG|`%+TVRCF{Ys*Z5߬Res Q3 jQA4Ӌ<>$.7$C$pA)hJewT*FmKg-lm*{{v\ܲsJa>3_*ݑہ>V5|WG_>RR_YL!RFjz S5fځO2< `}I\:XiZkRH*4[(xX$u|I9̺TkVzl_׼gC%*wXR nY)N.9+wZ[E9ľWJ%wp`Nj[.b|JOsdW,R~#* ĽyFdwCp*L(8OelL˞)A vfFʹ.Knd~A򥾺]Di(i]YʯJߟ?>w[侾7KK6w"!eDp5V* 3VEa{:KoEDcɾJ#oOU44lTjFk,>{S?ýSk>Su=|j}T -SU.nk.mcŮ)RxbT<TV*yÙ<+`RC;S^0-itp<ȗ2IZ_0ȡVVKHWol9=fd jb%}DCy{sI*{ZL1r`n}+D_*Uz3}i779_kjxL+u ;FxL.mmQ`sKzK#>&ޗxiBV^\s3_XX_رC+ҭj|S kϽ|j|[X +SU.nk.mcŮ)RxbT<TV*yÙ<+`RC;S^0-itp<ȗ2IZ_0ȡVVKHWol9=fd jb%}DCy{sI*{ZL1r`n}+D_*Uz3}i779_kjxL+u ;FxL.mmQ`sKzK#>&ޗxiBV^\s3_XX_رC+ҭj|S kϽ|j|[X ΆBL.?\DCqߢ7nO(M&JOiݖw0IJLM,NCOYPoQRSTUVX Y#Z:[Q\f]x^_`abcdfgh#i3jBkRl^mgnqozpqrstuvwxyz{|}~ˀɁǂф{pdXL@3& ֜ȝ|jWE3 תū}kYG6$ڷȸ~kYG5"ŵƣǑ~lYD.оѧҐyaI1ڲۘ}bG,{W3qHvU3sIa)\ Z,      !"#$%&'()*+,-./0123456789:;~<|=|>|?}@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`acdeefghijklmnopqrstuvwxyz{|z}o~dXMA5)ۈʉq`N=, ٖɗmZH6%ؤʥwog`ZTOLIFEDEFHJNRW]cjr{ĄŊƐǖȝɥʭ˶̿*7DQ^kyކߔ ,8CNYcjnoldVD/h 2 @@ -1076,7 +1076,7 @@ w%5 }>z}-R|~H(oYpq݃^s=uPt;bvSPPx <>Ay-|0m{opzrt?s^auQOw+T=y>,{¹luSmoou{psGrlatqOvk?=txj,{ @k mܖnlprxqؔM`WsNuȌ=&x,zj׫4lgmomqq0_s*9N uI_|2so|u]}@vLO}xT;"~z-*|Ly(x*yyr z$y gWTaˢĮkTd@D\dPPp-HG&]30;sCg( 1DE*n6ܵaz*&>P3ĸg| ,X񦁓`S$>BG DǕu#i#܌-`xJ!wم:(`[HWeQ2UFD`|:Cd2~TvkdEeUb2̽p ʠ~[@QdF!7H$ #dLt!BOK*G-iCrB.UlmO> ,B2W<+367ߛ@ )۠&KO 0ޏO igm82=D 4FB[!AIb4~Z *fz\OtF&ӝN&3xF[Hjz&3n14bM zB! |+ +NL؏SEZLnÖ`=lva;(>̽p ʠ~[@QdF!7H$ #dLt!BOK*G-iCrB.UlmO> ,B2W<+367ߛ@ )۠&KO 0ޏO igm82=D 4FB[!AIb4~Z *fz\OtF&ӝN&3xF[Hjz&3n14bM zB! |+ /hw{V\lsTjg?қ۟u 깮D}û.5ʺ(wM ұ=Ljeo(u\ yPXƢ8p2232"uh0 ;(3-ybݷ3WdsF@w ,8#!H*9)iF^ P7Dg3I33D_)JQNdOm2ta':=J.۱ s`d+uu- ǵiȵ\L @@ -1088,7 +1088,7 @@ v|@Uv lEx9 {XfPg@C_[G=/5g4ʥ^E*z 5#p&XsY>>@?nC)HKс#Eu$%`^>[ (?`~^x0_+OËv&"YD>s5x']~-if~>NF" P^OG# ǖ0<7ӆ7 :sXL!kݱrx{6Rt"+@q*7k1U誘Y}(~\H`J䞂\ 52[{F;Onݦ *C{2Hpuw0D(MHOB$vKѻX{'V' 5c +}3!JH$#h^Qv0qUY:ʫaZ=V.}VZfsy ֧MP8:x-kն(+rީGSIЭO wiι9јy&z8,k.$x=rmRMRuMb;dw0y 2֤T{WƊ6m|+8EC` Gd]Mm"WrS禜D~AS +G6W#jnA>puw0D(MHOB$vKѻX{'V' 5c sh]T4I DGãTD(2BNlz9eB_ ݫ.#JUbGɰ Pc36߅!3?o/˼ 4Ta1l-vKWZApɾ<>\Щހka8Z5$GdW#{{ߢ! e8l&Vlu4ʚ@ԸQWJ"쎛)9(6gf y'1?JL)b쭢l]4LkۘPpuﲹ)nCA Ŷ+2dEH'Hm&Y3uѷkѽӭ1n]_Z<ڮRvӛpjm9G݂#j}dA-uڠ 0\C"dhK>مٸ:IFq\BVhF'$[I&3BtK\ D'`;I ["%#N\I @@ -1186,7 +1186,7 @@ K.?)- c7Rg4tFZgaCCQx!)(04e) T12Υf8E‘6G V^؃R(E֘Yՙ >6."4Fm Iz9)d1 ź F+)mju@a7gDfFiUcԝRڊXxi>6|XG/@@+$kaQbќ0/nMҋ]%:c!רZTxY jq4Fּ]Xyw?=5a'v:u]㌵u=,"@n9 $$!+E@AHGBBpEA."(hA P뷙ӗ}Їw oPEiԑ9qͩ[ q)Q<\Uh.gY}WS(35QEJYj)zS h/Pk<^~'?aS| A :8}F/R+|cha - 4Y^HjZU7 + 4Y^HjZU7 [C1 ?w<}Aw{_Kyē]Pmp\+ؐ- TźˠRVYĐ[tX;-i(i7[9GPq4zg6@0=4kֈ\c-MANTij *A+7V |ZQ4fmld/ 5@ ݽ#]w̋Usri07mN wˌ|!WQRQIc fWlerU:Gg&{ q? n. |f0rg$u͚B869A$Vˊ:bVoi L,EUJ@!Og)Л@v4>4=A[+g $fy4"nv,9r1gJc:5J-AYL @@ -1298,7 +1298,7 @@ T^e@ il8;2-̶Ľ8r՗roو Q?z5YMAJ"KA 5 *#pL6#-pͶz7ӦJWn]Rc&S٥";H+,%p jHVJbe)Qa^b(,D y)|Z)qn3כ X)a zmVoRG,K)kȫvٕɎ|3LV&V%XU?@Uw(1ſ!1Ő(ZeW0Wi x6}=A{a.'M6eKȞ&!>6!$.ݙ[+tOfUUFW#ȑWy{R"wypьÝs8>Zﵡ7"fi-hgMoKKiIuHl7Iz7QCi -n +n \+k{'B>p6?7{qevCd]@?ߓv> eЛbw8Gv廝xw{S;|)W[E?r/~V迒g9jfjk`s@=aSN3w1_3"ܑН]QM^i@AH ,!!   aȢ ѶNjkkGfܵ"hE .qj3/s{{sfW/=4rl4:&eUԉU'br(PV_}P#>NW8,9u >K~i]ԅ܋/a坟ÝyDUD^Rj NOD{Z\oO#"V7ЊwXN)iQOͿjr˹jʺZ\25/$7'6}&o 7}״Gm:i=ic l:;wP^Ս Ϳ㌊|QMD[}fpNۊ<zǷ1tmk|cm_blԶݜǸv ?6OvwP;;ye*pALdRԩ3vΰOJuvuO*vt/v^^ٳK޳[s.=͐^cHzak=U>GhùwK[w@9(+JcԾ"_L+)qZ;@U=h̦E;ȇ#J$ëpKi נZV7n7ˁp;8]~QBi8 c>H7'""zBJ*'T"}kC]dR!EBXd/48pܑ~p֑ ͎,xx5quoC('u"4c )d $L.9t?$\0Q ‚̷C|n Pݠ}f>g#Ѕf!8w @@ -1363,7 +1363,7 @@ AC :U3PŸ@7QMVen)wr{q]yMWՌCkp^øZsƝ{=fKm`f9/c)QDy P+Kz?'\z?#qnU듸c>;sC V}ҮBܾUXfLXD%L3lw`φ1H6G[g\qǜqy,wy"y_sW8-q;-v\#,s:Jvv:;9:wlqfLm|N:h{u A!8bnqm* [u_epKؐ**2,m7֛l1l5)0.7TJ6 W\:dk\^V2Yg`(vF#9. % }#cwJFscS[ŋ6-X f%YZ=_ڽXU9 ֥t'+mZ#PM88>(cEV~O8qT oDѺk6+Y"ʐ-ʑg{fzmS,maeye//L:}?>4sЬD}>͟Po ;`k@xry`A1Zѓ٣L2eit,ET-RBg}[~=h(1:ӑ (X2 d 3lj2|/N&I I?Wbиlqr1_5׸S3Xejf<.iSGCp" -80(ٯ[u^ȉ̘AȈҢ}QXqR9Ӥ'S E ]|j)ǻMk"-&1sT?pjPEq췍Ҽ3NZ,ҿqBj;(v<.@0wlpvL8!f)xy\ԨLȵ" uyGEuqwgfd`.誈i*e60 URUZb2XYK(nQ@M\)GO-hknQ999s{}b<31=uO\u]D1D[~:s[<ס='ˍykP0e P0I(HҜy2s&3.N#56CiuXShvNޠGGp>36o_kE QY|7jdYc?4bIQ4I\tl-4 6)1D")!ΐc/T+b۵ \z/NFŋ~>\3T`'ٔuy%&G,5E^rR!+ea򗤚a6IѶE +80(ٯ[u^ȉ̘AȈҢ}QXqR9Ӥ'S E ]|j)ǻMk"-&1sT?pjPEq췍Ҽ3NZ,ҿqBj;(v<.@0wlpvL8!f)xy\ԨLȵ" uyGEuqwgfd`.誈i*e60 URUZb2XYK(nQ@M\)GO-hknQ999s{}b<31=uO\u]D1D[~:s[<ס='ˍykP0e P0I(HҜy2s&3.N#56CiuXShvNޠGGp>36o_kE QY|7jdYc?4bIQ4I\tl-4 6)1D")!ΐc/T+b۵ \z/NFŋ~>\3T`'ٔuy%&G,5E^rR!+ea򗤚a6IѶE $}LR¤r'Vaܦ 7w 3wY`%Rf5Q|'&`_ԥD(9IQ&1S9DqpŚjdt/ a44ztc-Mh`yн\g̣:0+*"EPEaeXT7( .ǚb&Zq_c5֥1xXҨZMD? \{0t^>|߂3s1TG9y%41W1~PŌV1V$ٍ6es[2͔-WJ3-WAMJ`?fr1 6 k`T78bEqgO9+h`U9Kq&(%a,pFIJHc0'+ ?:cx#%S3=|K!1'tTN쉽/[P%5)J)Iq$[d-`.s\ŧ<+SJM2ZbSI Qg[)#Si)ZdQ5DJH5ʜ4LiK+Rm9-QtzƦoԘТKj0;1Ue -v~ۘ7m]Č~2g V|F2-cY1YVEg56@cehKoPxve +v~ۘ7m]Č~2g V|F2-cY1YVEg56@cehKoPxve G r+^eti)̇ ߶L3zhL9eHc_#yVR!: qq)ˑLS,yJO-QZZRL#}R\ z@IGeǕ6|W<h5 ћȅL|}^d+ W\QhŔX]tȑ_$4(c,J*t=TO\K%7MEF4 gR]AQg]wEЪ(* --, +-, BmăD3iFUi;1&ͤNkNc̴L56i֣c,d?Y罾}FL+`WJQdv|dȕQ Jv\*C ~;+ιOcqX^8V±`>( *id_+;IFYIJdT'[y*u)ڋ'/ыp| <<_h&q;(@1τ;~$J ~dʼnJ**@0 :3"$ * !yURxP JlL_qÿ~Llu1JXbPt|R.Fz#ìCH Njų#aKgpK-/p PH9ĜE̓}O?/Q_µEgKO F+k+:w%KF.(\/Qu`;ϰ-DMT\~vPBsy&1O _?f4`9VAZM.?Ppxs{Ez3r [d!m\@̳p}jΫ)$C7XlaX?X6N`LM6s6U|RMySpw+TQ"͡|ի^3uK a·A? XWY q/O=r, w}qKCM~'q~g<>,O ڙzb/ku?#|agD:a/Caq0&Xku7F4(8!8G䠿&M sA ";`4"hu&x`x?NsfO8)w /:r΄;M6HhD9pɈH#88rpu\,b%% @@ -1541,7 +1541,7 @@ Z b nUƱ3h:Z+neJ;=HYB6BIH@P !Ѻ/NT;նK2x:ɇ0p=!?}f^LRpφ`@Vr@G Aw"0<A!\ŜԪX<71 1 '#hGw_C0" 5m ṫ` ",B",BPGbP !BpS/ t3Ϟߧ"$/0` %:BrXa`F6;XApٕVb\r>i:_PK -:G/Ґ9c+.q|h"|X ~5.5uбFl 0a|x=u04.zE4)x C$Hl- yױ;'jn i\ W8tl-бk؎nA pNlEMlaY6{ר` Y;y80_w97=Ecg@Ҁ= бQR$Ή {P1j` B΃Vݕ Yk`Õ(,7U -U+'F|` +U+'F|` ^EMB@n/+iQ'B/ paT/D;C!XB"0cr>Q88/l0݊M?xy~n07|cǎ0q)SMs^(d^^2l/WYn_zWl۾ܵ{"ވ־o|#G?>3L6ğ=w>1)BY"D-U5ڂ¢CiTSźƦffpvv]|nܼu}ŗ_o~OD%}y1<\'_ gK"0X8d$ D0QPp)#`@L6-F8n#mO@zH(=&c̾dݽz~x FEyy = % G X'$`(,K?W-=C o"[ ;=Qo;p0ȱ4Ï?!Idr -bXwAWM1 0 z޻}_>xo=z;xɓOkMuDT__ba~CٖsJ:CR Z G#e&\WfHKi h0a@À 4 w|kfdKeUh_ݯAųs94HASe *g)AxӀ n_ToO*HSoTb.W]ޠZA Р%4(ײ3n膆>nE$YL!`*_mԝ/QsР 4y"ySIfuaƹgc,i0,5pCu~S9Ѡriȇ۝+]xWY"Z:ӸdM3^Dv 97V0N6CC4N۝#>1tdBG*@C'ie$5hͥotРUrS!\ʖrz$N:Ҡ#{脆ƒn#Hi КʷkJ A˱)sNy6K"cwgI=q:E+6 Zg @@ -1637,7 +1637,7 @@ t .,* l!!!{ I 7kKGwKU#-X+:uA=zL[8 B|潚|w]=hil*5{.]0wp3GN RqU"֘[>asbOn"){>G6bڸ-Gx}HY|HC4ЄaX(AQ> a@TNq Gq2͓$ߡ(2)*%`8z dE!; qL.}6D3e|4|Es262'aqh/Ȣhf3 2* (\GAi,; <As -Ru t:3ALd> 1y +Ru t:3ALd> 1y J ' JCʀÄF KTaP-!DXK/ldAV'ɺ.g Ivg|[xbd=xM4d'ѡ`1IgB'^9pGCI<ے!ٟ tNf@x&v.Ywg!>Y/yB t&xCȀ. &E [D(@/8nBܖ>BE<C!ρ ُQx /(#hPy#o1&BPPCUꓠ4 ʝ =GBH#3 KGR9 &'}HNJ1&QOn=[}KAݝ <Ϡ#4>(:qLT}å A1(Iy -|v{8TgP^RWhʟk4Owyw:?.)4½a#*}P23L}*QhAd$?ҵj}jzoW ˦QӅQ9g0"7x&XśU@|e渱jGʰs)wtuV+neEc88ᑾx_~aKyrpf.l=tГ|{]Ċ:&N'ؐ=ա#1+mWU]GF&K_ @@ -1697,7 +1697,7 @@ B, *22XPHTAD @(JޔAB6=sfκ޽{b OMbCx-'I?&s>XߎM9#sdkQ5nBN y\8 <; ?QB+ y#p!uNxʶ [Ÿ] X&wg<%ݫ:0/<8S6|n:9@틼H뉸Axh|KD~F!ZS4.y} |&t3I l}#fr+Ȧ0k4f,9nD$s& J{jUwQ1k n$o<.x:rVȖQF"vIv$5 -Jst0k울 NeNEOU{JX( Z0D] +Jst0k울 NeNEOU{JX( Z0D] (ަi0E&pJהFߍyǷ ʣl2v2&%ݵI ť3ɵD K%)^U / @@ -1707,7 +1707,7 @@ E Xp;O눠kXMaщԭq-5ǷUWFRW%TVzeRkYE;')O'̝{/!s[Y)(J"j& pk0hkZ1i8f .ZU*+{H˔Ԥj<|/_|b +.1]$[=gp{W#vVvYB{>bc'ٸQ9jU#'!@jYR.:S%񫚙'+|*'88|"*;R%S"h5[KLqf`34&w3T1Lz-#6-.Y(l5+ȼ&WdC#- n Va#FpV#ZX+*_ͿE{Wp ``#6ფ!ly +@N{Ss\»JC:՞A=q;mAԣ͈zL(Auy{oq`w0@-vвuq1Q -q/xl#GN *v:s9>Վiq\r@ o/"s;ٿ}52GpsgN kdӻ iWRX0o39jUmW;'2w(tێLݒc} 9. ra ut 4|$@MH3v;b=IQ>as7[MΦ[sf fjvg:`Kږ:duȎ1{\E+WwA'@?@ίXΟH m!f[Bਞ_l쫏^'1)i}g6Ky+wVn|8x8]Mh_ο-3'pC"HvY(9yѡY&/J9hZru3W/~,=A}ny;P gD.~gЗL{(m# a!: 5px7?ՙSa20 f`FP"JQ,X"q%Uc jtE=.Y{uƵG"%( -!;O}}'~$~0Ofh#v^R+uBW e{; F;m_ x(6Q}اD֍"j)]5GPps`|(|H?-"")bϏ߈5X/v~nH>6J-߳* .C4'DD8?( +!;O}}'~$~0Ofh#v^R+uBW e{; F;m_ x(6Q}اD֍"j)]5GPps`|(|H?-"")bϏ߈5X/v~nH>6J-߳* .C4'DD8?( А:H>0ZArCOY yJLX R`Ev%,M4/q-T{cDAD 38Ӆ\Q缮kD{xv;a="zNϤB 4MC rfh a]';m$gxF[bFl6_7 o7䴺)AU輺ɡQA5h8AzvV,Ns!eL83 Gx*NgLбB㐱Um kpooȱ>^AwP~1?OH1Łi=3LL{յ3OǨޥzZtnT!ACӷyFsh"D3\p-Ds8I?DMy`%6U" lBgE b eJ2L^U++fMOe?Y-k7g]ew+bG)F)O+a5Xs\3 )ς@x+܊f֟btRk(j/˔? 'ODT up~ `$lF򙔱xV2eы,?xO{*PuAo_t?_#?%7j`X~|0^@0WANx絔Ahieޞ`og?hΓ|9g|Ht7B|{`'  zh%hp440ppX%B0H1Bo FʗRQ>= X=Q[LɅCy+)hEˉH #[!`|E~\BAYpS8RB7(ˉ -ro }bL x`B/Hb͇C<hƠ3̕A#z jAM,H`Z&)&5t>2L$U)}~D^ KK0hȠ ]̝ACo l`rI$! 2A%r|INeJvv :2hOZ1[•XB\RJj٨B: Bw,\'u}GEugqSFA"3u +ro }bL x`B/Hb͇C<hƠ3̕A#z jAM,H`Z&)&5t>2L$U)}~D^ KK0hȠ ]̝ACo l`rI$! 2A%r|INeJvv :2hOZ1[•XB\RJj٨B: Bw,\'u}GEugqSFA"3u DPAd230 ",BK5ZWcM=hbY-b'su߻}9(zy'V&q_ Nq%]ev^Hihde-r8hQA:'hE"[|}mqBLb?ǖ( zŨ-,rw( e}ow?$kxo%7WCgҋ_w?=߷{'+E;oKQܒ(['e8s21E3fNPxpz]8oW.Z ?Y̬ Y 0/2]7\ g'\e /p@w$@/#@oZP/^z~>+]}A&ݙ;U'Eb;w>3_q)0JƧ(:@38]z~@Iw}҆<4{~ެ>;ܛs\Z&Uٳg7'dY>=x5qχ&G<ޚ~f

    #z}b!\ C a ZdC_E yN68=qh~y&sL?ݢ?`xOn>A]gwd-MwN6]V@A`Wal-pM9G2p:ҋ},b>H.p ,ݨ?$Ev/6߹r{Z6A[K:K7]`'QkԱO/&f~e%<疈JGT؃q=ѱ{#4=]7nmtۯ6lM%YK#٪w͡hOPc8O7cq>_'d$8,d_۝P=>Ұ;.AԵ$lSlEGtmMֈ6eY˩1sC9z:N(#5hWұ0e7gRYp" S'g67c{g7'upKJFu=1Ŭ-![ܪYĕ6/Yn"UVѩ6̥2+yy]7Li :Ƣ8н}I ڍ0۔)oS1ņ,؛m ;s䬞l/^g\Pu1$U)&uMCR.־:acE|sejkQ)Wjvţ3q$2 @@ -1739,7 +1739,7 @@ PFF (2UXEj/-唞ה^dޗd3MD1AJ^W%fA=X4By#45Zѫ ޥ~E@C]S_kͭif!azSz;\Yu:\YHUITf"P _]AxkC?4 -`Cz'f,@w +`Cz'f,@w ;kW j0\Ž-nؾ$mˉuY [uMeW/ة)ZxM* u]xpNA{&q38;p;@57h~D@t[ۛ NDn^>pW BCȃz`uP y2cc}8ܻy3itu` cOx>>ޏ;x}~lFຕ@Cq \֥)bJr:ɣP-g< <ܗ\;JܖᦼUp8^E' 霽:'8^vMm 9ò7RN-F 6`|KZi |A|lr :)wr(4KP -Q~RpBpNˆ/B׏܆hDѲ!|PNc%|a#hpJF0ߕKżbxLq㤸Sn~ǐ Ѐ (%]%\a8g|JHlHOq4di X65|ՆoexhE0W!Д./n{y೎Y|< scZlŏv0 :rEE0&u٦0k@3nIk%e o!ya|HsvB!'KA#KYd>`]*Y Ճr\tuL-1GlkKx_ o8I/9kA!h.\c 2ꄯMS~w9Xeqrđ&fNjn q/X6,ao=puV?&kyGC&g3dL(9!Qjgky?ۇG>-})wžk) !#6ko,c\ɊA(fC~yCv&ړ{OK߹F*JyW=烀% qe#3pH\΀j<9y{@&/|N:gT "bc|'ku4Jg-_-__߃?[mrrz{ҿrGQ @@ -1921,7 +1921,7 @@ TEr-ZF ElA,DBHHB@¾- @%1, { E" ZD|>^‹w>LpP8:H"Ol@H4@D E> Eߧh x 3{ =p~@bIb)y`o%65~) }OztGr( yĆ_ x {L|@mT+5s7*Nȁ3GR]xڅ)|9x^d\ \dyb.pPdmkkm_"8'q*)c{&B?P|5 -b} \hXL35j1|%/h`?b!4У( ^@9 Sa6r%'Pb 5A(=)|FIS|F!'AcIn#V4jBkn    {C.bE aԞ=ag*"tVP*GhHLf)sUQ͚H%PY~5[y6V!zgkhpwK +b} \hXL35j1|%/h`?b!4У( ^@9 Sa6r%'Pb 5A(=)|FIS|F!'AcIn#V4jBkn    {C.bE aԞ=ag*"tVP*GhHLf)sUQ͚H%PY~5[y6V!zgkhpwK Ly}\DM3 3]WdkijJ$#d=U$yA 7B-P%P_6`ׅ3$}N+5AIL62U`#%yWFSE E[V\Ks2[nh`ԱCbx. 蕽|Đ Cű-NV>ߊ=jVy& ޠe=6 S9Gg?2ɃVEnvK7rk. ^H /usYq[[ 7sbMFuaӶ9Y k -U E┆u9&NMit tIn3 .3^9w^SǰVՑ|פЭ37\X%XQ"L{~:ܮԶʬymdmC9TeZl7$chI-if+ qA3$MScCV{n Э _}#/zq|εIJŕJ*-A#HOr{kJxBrA7좑ܭt~NZn2I##rߒwYQ۞7<{7ú{cDfknNS2KR2LH.$ &zrX艨Y? ^3C \?bY>.Ԉd#hݐky˰qooLlqd)jMlp-:2{-O)zIH8!J"HX>--t?x9g#0 ee:@i5 !e96̰p\C8hՇ" `Bʂ,R)!*Bk ;[s@\#/x7){4<ǃ.fqq!cBE1"ST,B%W"=U^jo3v+w)n࿡) >HmW] f{!և‡ T\cMPCj1de6C!>Q DHhu«)5QʤL @JM 0&jTًH CָCR6 Qrq Zلq鄏 gLQغ|AACil2} fl)2HϠY_8!e+<8!vCKľāIp1\h"G$wH @@ -2024,7 +2024,7 @@ s 7gUTu|L)fxJ.ǽ']yAoV)D K1 ѕJi=,?', uҧXKStrGÐHS"2(Zl#76u{]v2ξַre~j>9XET2K^Q9A~E6fLH@S@V@((C^ d!o5n3aNKǪ]/wt9Q~uqXN-Q1J8yj^NB03S?*?g$z$ {s羚P?llew]O~{GI=VkȉŔe&tir:NfQJ8Y9$Q)JRR^y:m<$HX|\^ԯsv-K.O}ݪ7rV>VANfgf1ӋjMOu^>/NPJԷ$?IIeH\@\k X -:G.5yionm>KWg(Kɔ4m#%']ƍϬfeiƴ$:7<-3.#%. lb$ zob0[=\&:7;/j;E W_v_U*є(ɧB-K^P•Djω^H;)ʋ*|˔|J d!"5.7 Eg0ri={vUM-ݥ :gv_cĄr5-4uFxb$[ܒ\A?LQGl2d=iv'poٻǩѕ7w6qiD>YP|]>&Q*4qw mWB-PA;?k#t{h5h0phUw-uHy^/;,1 +:G.5yionm>KWg(Kɔ4m#%']ƍϬfeiƴ$:7<-3.#%. lb$ zob0[=\&:7;/j;E W_v_U*є(ɧB-K^P•Djω^H;)ʋ*|˔|J d!"5.7 Eg0ri={vUM-ݥ :gv_cĄr5-4uFxb$[ܒ\A?LQGl2d=iv'poٻǩѕ7w6qiD>YP|]>&Q*4qw mWB-PA;?k#t{h5h0phUw-uHy^/;,1 0idҌ,M6ɍOhoht#$1a-0 pF;0r]m3`fouw)^lw+{/J#E] J ˒B'Q:*(v#-3>xJ÷!a m̝`,߷A ,hrO-i~%s0ɇv9-t9(ax@!p`9 l ,n0aߌ@v;(ݎ[G%];1MοÞOʾ\O%(wƁfdlfGmrޟ~n^BL Ѿ"4 I\dLCpfbc!Ń5RlOh0P¡Ej9Nh8b#MN dBBgCbrDd9CVI;hdFo۸O@p꣞Sc>k ᳐ENCP@^ᆌE gސlB@|<:S!RԨ/Do/G [1|l hf;U:A=*$(j='os背f2N/d~~C]'^OaM)^Rq|m$ y$ rȋACrr]3CS2.TkyP~@ȏA~6dJ |:a9z[ gBƄ>c8i80 :W=79>Эvc4ۂnAPAu,lȗQ!ѿ)^E*T'* d#d@G4LX( aaV4D{%1K튢|O"Ə y%~Gnwv?DsdڐT ߳`F5}E=z&L`dcn= @@ -2032,7 +2032,7 @@ d b}_`߻FܽR|~vLU k,WTy|zߜ_);qc2i@Vs%dM}Q ỐOsڷYVp3? c> tF}i1\Ci`mrkU{*7iw<狹_(o3~n9h2m:oHXնf>L0?I8XUA)ғ`B(@( =jjA@P((2눸zQ 3{f{vV|>_NnMF*1&8xxot~ |NΌъXQuTY٭9.}|gWF>UVJoO&51/'&Tſ NZ 4D#/C +2TVkrRtLgve뢰%1Gz ;ryr)R~1)ܿ>YƪLa&KEļus->TC"{٘p#W7 ưnA:hO6zIw VzE':J 9U*%IeQ'Nt=h/L@ -TP![ ד`]tk 5]Rwҗ]&ok7BۣKq-IM79'LWBȎr0yL, +TP![ ד`]tk 5]Rwҗ]&ok7BۣKq-IM79'LWBȎr0yL, M1e?0Y~rD#CdUV&z 0_@]=hxVr⸁pD0`ƿޟ:esZdjJh*dAC1b)VO(P T{kn~x{oeFgC5='ݼios~)wC,D쏹k5t$9ǐ27zO17ml36E|blՋ6<ȕ~WNP0- 7HP5 #(^C}lgIqLO΅sd?8{ &`V`ǘ9f32g̠촋#:JR%n+Wq gC5(~/r!Z Ɯ% Ygf~,"/|&x6dtmGUnߣCnO6p`sY9P@ -HUY.B )RyLR7*71[hP),SOeNwen6sew,~^p\O;Cde.|-{2!aYb3V]5+ꊟJJMWӌV}(>o;6kb6ە/a+~*p<@k:> }Kې_|4kC:(r:k!T 5C^pZ>}w <H[_Hh \~:L:IvMQ" ְD{P9Jڍrw2Iu|u &9+m8)@ g)kHE vȜ` dYTg;Av5&@ $$$6!)67,E*n8RA[EQ}k=ťӊ֭Uq3_ۙx;}srpig0 bɴA$ @@ -2078,7 +2078,7 @@ n Fsr-PZvs-9!Ek)rkCFg\ktqC-sL_',IwK]uҿIoqɰ@p'؋iqꞀٳRcviy]+!hf-yA&uJUtYJN2ϮJQPd68f4 孢ByVM#vϓtE5 *08LzпdcE0`]EƨZ?A-E3ɦ@Anܘo^fUδѫTKTܒr8α kPIR(ܳ7ܳ^r*s1/7;Lz>q' AR :Jm(sEM^ğPи(Ƭ Ų\+gjm܂RZW#P BUQVW-Su=#*x!Tc' jLޤQ8' •\CT" RFM.P1ha, 1(6-+Id,[MQPSV ;ej׻ȵe=nRq4ew8],X\iP%28Z2 XNvQSK髂X& Z:\Q-Qg9(J+FuiWIcn(YS üIwypB{a4ܷ 7XFU7(oE42 ÍbrkS,klEV^UM*U.$W4 *\ݢXq04k 3!A4(ADqpU֩Z -X+ +X+ 8"8KlUk+.D:Zu}_Z?y}r?hL1+'Ǫ$;LZ_3~Pec^:A?iÖ8g~&h;+Wƒ&^I>7AR9{u{d*`}¬4=f15x/j\jѫ;|v G X{EwϔLmn5l%$ ݓWoy?8lJeҁ529ega:__qڋQq=C89NwnXۚ]2xuj8QgCls4و<+al܃?/ b, q=0DGcr504/"~}Ts{re.r,EvܜGD7H}zQI;q-ri9Ѩ }>mmV ⭱5^tWtSYNq̟Xbߢm_6*m管;k莿+gs' v}8 .B- 8Dz 6PF mmmmdۤ}hͮnyc!xP:շn+9 d ;H΢l@@ѺEA0[TV%=вdKƠE4++JDuIU>%Kſ+ T Oﳿ&3{_3[_ wE .R.uCKɗ"ۡrAWY E ] ombj/e?fSXhf? rh^U?mwfpػ>pbP٭P؋!vI/3xG@S` @@ -2102,7 +2102,7 @@ IV 鈛݄p#?(Iёe=>1E;w<;A&W E8>UQ1=H?y,NxdJ<2uQ-R.iOeEBvWjz/+/ x=K{+~rK NX2Z*L-!Kel%]ϒ%#/X |* })v\UlSl}Mbc#?4esZ 4tU\q/Q]}IEcdOΔﰦ)[+ZW(7[sUͪ #s5oPtU]*60>kt&T Q?wQ=F*Nm %4N)h"/_WfWdkr6hvج o"nYo̠6ABmАc̿B$Q~<)p0EaWHiCxڰܰъ_({NV ^ -]dLk$d>=H(aAha^S}ZO#=vn4ݛjfWpj/s'Ϡ?FJ׀7GbCdr#H91Pf蛤^'Ygi3lz2 h8;8R}J_#6{܎~f췏l:lvژȉ醕1aRVtYFtbaʅ&-jiّ" )+G7Niq4%CrcG ;ғ=FYcP'pFnXoEF|O v"-6Q͠hfLΈIM=ߐe41zWCR[c@a [5{砚}>)8 +]dLk$d>=H(aAha^S}ZO#=vn4ݛjfWpj/s'Ϡ?FJ׀7GbCdr#H91Pf蛤^'Ygi3lz2 h8;8R}J_#6{܎~f췏l:lvژȉ醕1aRVtYFtbaʅ&-jiّ" )+G7Niq4%CrcG ;ғ=FYcP'pFnXoEF|O v"-6Q͠hfLΈIM=ߐe41zWCR[c@a [5{砚}>)8 |`BV `)-,5!Z>ʔULM7]?1nݗbWq\>r{c ;ғm|/#Y.h=?goÌX<5/e GAkТ!#@ Az@TBt]OZa]-3umn~L _|?~i扫t$))2k89ǹ0ՒJT2k7gk[=LڃYSL^&3iH$%QS{ Krٻ>5`:d1UKkR$iAzc~97⚣[XVu'4i^ԛ4#uNpK J?sYIjeC?14LӱظP\!?kԜsr2\ VAZwmꔌ5I^Z Iz-Y/(bkی8(bq1;¬Ay¤c> xc&;b|G:1SYQ1#:As9|ҩw X=|}鄓2v q~ x́GO4=ˠ5½ @@ -2141,7 +2141,7 @@ eʔ$ iG}dc͝> 1cx@k[Hb[fB+:q#1&.헥{Rh2q<3I+s#kvxa>Y=DlvBP&-~,"d%ĞXVjI 5bԴc1ZiCvZ3\o1\r{y{lb>Kz 4&Vq.]#4"!RhX0&>'dӀ~M}̽5G%]3G%>4G%VhdeT>` 38E<gTJ&;iHbR48%LSh@jT6Q}Ҧ+:mҲ+3m),)tUShShdArCc#˰ Jsz2gکOzguStV_ٱ -ώS +ώS ˙МSPE9kS+c͹,2L/RXݲ|އB}0 f8*]A , V@a Zƨe|,3mIU7"ue<-\GacFgWA+%r:!-;klql}Q3dcMW2UP #[yL@^RE7_W?7Hq؃R)`+5okTg/S |!adg,@PՑXuw\ xº2s/)kS @@ -2191,7 +2191,7 @@ c;E즣(3 D !L`0!J{e7#bɽ6ɼ0Xa,L|qzJ] PSSm$;8D'!b8 -,FI> d0 1y7ȹf{5"Iq[\9 N98|_%~ / .) ._\Z!,8 ]u'0B(5wN FO3朜>dPg\Ҥ}jCtrt\\ȯkK8D??8{=<<wrx\O &5y vh}q- t=! P Fj0ؔf/TdV [=v]Ku_}K7펝ץc+ XASZQvg+tB-l7?ckncgX>Ntho+|+{n* ^k踂?t\B{lum29wtt"w71pyG\Vx塿ۏa @@ -2257,7 +2257,7 @@ uk 7gaV^ha,ӽ)C"G`CAN yb*c4Z̲IhWGV LgMbgXޅ8|DIQNh<)OzDDF(73@tiiNi63OLf1;CuIمQ|{b _?Zj`&ޱ.v?._Ə ς(38яWҗ>Tz.?.7i<%oD,!`R8\)`. .jȥHB@H1%폎@TXb/&f:.cK4#1wsb=8|LfҖxxCCxt $2N(mt 5&j0T?CpmG2aEh9K(U/0q&{@AkX = =Y&zfͺ uЭ>HV^iPfPwމЋlxH9,4ٲ5f` ,x808!qM٠)]I l"10BTI##P$Ccba܍2Sc5#&F&G;Τ-gҖH#D >[3F5b( Ab${izз9&l^}p"F;b2!{asE D&x#8j$,byb!p,dLY]ّ!1CHlp\q .U%NLH-Rdch ^@D3Hvgxq|Dp*жhcHu}67jʌ -?R#3I3< PS,> ؞Eq\=-R'6;9IAzɆٜI6|XdA,@W־+Y?[ړC iBF-(ӊ -A[(oq@j ȡ^s8j$,AE$h~?Xhڊ>ǁ-•a0|!St+R)5D@*zmahCFnlV7qm͐pnyQњ+{O#Ok R>5y]Nbs0 ;P^84~EJci ؞Eq\=-R'6;9IAzɆٜI6|XdA,@W־+Y?[ړC iBF-(ӊ -A[(oq@j ȡ^s8j$,AE$h~?Xhڊ>ǁ-•a0|!St+R)5D@*zmahCFnlV7qm͐pnyQњ+{O#Ok R>5y]Nbs0 ;P^84~EJcil)%dtUY#Wq€rFtGz](9dj_8`]భKJ7HKwsؗ1TT..(rۮѵ}4f>z{ϟࣵLAϻsƌzfzkfL(քC ~h?j}CJ3E%/c_TVJ*pT_xEy\_^Hڨ;Wi YA"ҭ[l!Iv^يR9$Vd2nqy>=/<y;s+Nw $ ӟmWy0\*c<0gלuN@B! +G[Yu?R|^rrH/坑,~$K]Kn`l=Z5[7q|gUnr"~F8ߛ-cY đ\ೖ-K1Es)`[>zyH]PF(볫ܤ;dqFV Lk-zPߔJK{wWy~P'C8d,ߴ. :J@7 dzqF@` V" 6X ##  ZeWŔԃN~a~qfu#E".lйy.?Xϊ ;m HK=`(tu4G!gn_:^!B@zhCLZ8l$@ @@ -2295,7 +2295,7 @@ H 7 ˰\\y\S۬T|j &hvp /S)zcZz9'7Zh[SlJKK5BkA^n93=ͨOR8x)h|wHrNEFDESp*/ZBqvv #h'Y&|mmGeUuMm^}ښJ56kAnNVFQNO;Ƨɸ[;esg&H)rOu7/RZDySya,/Ж?2tQ( :Ya[u5u66lkrs6w9[i޸f^Ddg:m\4aݗ)&#{.F"YB U!mD|yr!WOz;/\H*Mg-*)wT768;\[۽/]=ۻt:-͍ uU9#UViGq 9 &"KbȪ -j5=h"jt܈#t2ݝM70RLD6gb%k*ETNzTnKd ,K|mU݆vWW} |0xhᑑy##G `;\͍k*+JX3IJY$:R(~dXKdH<#QV;nhzyJY*EKZєE {ۻKR30IO)+*kZ;:wڳ##ǎ8usc;~3O=~lzumvnj\_(+.H>C]jOa'OJ6s +j5=h"jt܈#t2ݝM70RLD6gb%k*ETNzTnKd ,K|mU݆vWW} |0xhᑑy##G `;\͍k*+JX3IJY$:R(~dXKdH<#QV;nhzyJY*EKZєE {ۻKR30IO)+*kZ;:wڳ##ǎ8usc;~3O=~lzumvnj\_(+.H>C]jOa'OJ6s DtZ&6QFޞlwDdXTWXkN7&'Xo{[]@.Sqc\*2:Ř_hXWaSۻ[{wphg.\+W]_vg^0vяFP(/ 1Q|2"˦ ?NET֙6{u54ճQ^b6Պ8^WRXg)zSvԹYn7tjT,byKmzSkLK]]\^IwnЇ؅K^q֗wwGMNNχΗn_z҅3'G=Dlu9)ҢTZ)$͕9Q$}Z~66Hlij,+2jEa!^uWG/Ka"l1+-vMזX-$YlЫc;@p\d!vWi&s:sۮ8s7n޾{'~3Ocjꇧ͓_O>|p7]SǎzO?ŋ1=/?㳩>y<9q/S6gN|oOϖ-R[%#fHH v-"HD1f[˪];vp09:t{elܗ{4Yn$Bm$H>tdDZ%tlI4Jrɥr]OBh1Ƹ4c107״9 >Z͛8qXc檼Gw9Bdr梽p$@ܑ)gg&FdD?sdOW$Q&!~yTZD-ec<>9|/I@gy"*sێ!`a1nbֆo t{(~;&Gǡ3 l+/NyzҹS($"yvpPsp],Oߏ d;1P[]uZD l#odFUumC+D(}c7MY]{ 8&)YL-aI,X~ݬȩsl]nr㉽!<*b5qZL676TWUbEUYU]S[܊$)}4㏈S1$3*beGq!ˤ-+JyXWK}ǖ ?`9 nӎCmnE$W4ilćflf'ׂ>Ţ-wC6hO!/0!`ӈM~7lOڱi4!rPV2 l2yM$tQy}wp&ёaR'WW-f33SS`%'gds +^`J˱յ -xGg rDh>q!ˤ-+JyXWK}ǖ ?`9 nӎCmnE$W4ilćflf'ׂ>Ţ-wC6hO!/0!`ӈM~7lOڱi4!rPV2 l2yM$tQy}wp&ёaR'WW-f33SS`%'gds 1e:`C,O oN[o>}hewέ/>ɥW+Wฬ7 H2yE mwK嫴?.ة^QA~$=al Uvj{@tګʖnE~!MV\mZ{W^|CklcQ5tp{45>`G<@?Dhk3ҒDE`#b'&esJJMmb`q?43![Oh?CݿuR˶w1uZm&9$ tTB=e|cm=+;v;$%@N{NjFw59s B,IXΜrKT7wRCst* \_]y @@ -2325,7 +2325,7 @@ LvJ| 4W@DlrfޝQ b^Mu=.leT"Iygp!)twX|F~Ec0ggf}^Yzb0 ?>m%۰h зf8YX3g>/ w5Vgćy;YPsp4D(1GfIi`[R=̙[?eOukWse!!;YC,%K)+kPhDR֊VZvl)jM =w<9Gw}?u?dYʺ[jK2o^ ;u.";+#Rv(|rAl2NRf6bn-(k"I7c an&Z T:2 {=R]u+:㸅2$R5pIV?`vͬu383uOn^9fs@]ZP|-N7-G1Jxq[YE5-dyZWdc*+.ƌlF42~aM#fiჳg03!{=R]u+:㸅2$R5pIV?`vͬu383uOn^9fs@]ZP|-N7-G1Jxq[YE5-dyZWdc*+.ƌlF42~aM#fiჳg03!cSG%?(98֚' T!Dg^U+g{OjZǦg~79ArcgB z&pjٸHtn~$+Egk$ ?'8h+V/cd&pE*iE'{T9JVh(Ms @@ -2408,7 +2408,7 @@ Eږ͛ aiSK_6lg䙚1]kp|f~C`jJr3{?h,+." ѐ#2!AJK4 ^DRNYKIˁa1əe]+GgI>gmɊ he"m5G$,mhZk^&F\?p@MVDM-Z`y噖ù+cRk23 f4/39n%7' #uiQA^D{ C% L;#3+"klqū"S_)k#čuP?㖆#(RdDq -H(j9s9״Ʈɹ/(pWcE^گ!1VD1"-mj  Љ `WK)k]f +/x{/`v)< BCb72_(@ $0c.>A)w;glh&0ڒ짱`Ks:hSQaG<y#,ȤWum+Mv[]-w'V /ϸ +MU숳gG[{Wn,|hDOSYNRDbabpU& `B ; +H(j9s9״Ʈɹ/(pWcE^گ!1VD1"-mj  Љ `WK)k]f +/x{/`v)< BCb72_(@ $0c.>A)w;glh&0ڒ짱`Ks:hSQaG<y#,ȤWum+Mv[]-w'V /ϸ +MU숳gG[{Wn,|hDOSYNRDbabpU& `B ; DݦV'\݊}WRwhIXf'{ZJso\[6a^,+Ȳ#ȍCˈK(u_=<5?~@<75X{娙H%\ 1Мbr^ | mihYۼ"VN_Ե.K{av|urTE'+C Y%\ g.{72 *F,L u4֔L @@ -2429,7 +2429,7 @@ u ;wZҊNr0&Ӓ  ,/(οey''\cي)/y{bR,w/W0BX0P-Ŷ9+ *k+ o_Immh$GeYy  lv}E L ' llԇ(=Ơ vw몞<UNr3K ,-m3{O􌬌ĘpO{3i+b+9?A5)Y0Ttl}w߁Kp%;ꆦ֎n2:Lg.>.cnnnLMA10PZ+~)˾Uƒx:o65D]V dس,Ŷ)#8XF>VrA&z}c-;ݼ?~’5 MNR/:D8_ 0B -659)O>aFajvBSCMӒ»$B}{wn57׆+2pc[' ,2&6.6&2,Do6Nrm5Ҡn +659)O>aFajvBSCMӒ»$B}{wn57׆+2pc[' ,2&6.6&2,Do6Nrm5Ҡn /@ qq6kl1B%uu6 xeg\v葰CnvZ*k[q#d-WTV542>}6rVNGek/ m=}C0хb2:mxhpL""T?yTt?/'rGCv&`)+JBXؖWRhda F5%y8~^F4m=(sHDKMʹ}Aqɓ_+_㛚~EYw pg;00ܙa ( `B @@ -2469,7 +2469,7 @@ z *vQjT?g_3V*Bm.Q4;}R9O(?-C Fx>}X10j =<}5 dlD`=螮PPcpVī£B!wxAJ<.%zD-Rb$#/5y%nHVYL!ϐ,ɢ#]rujWҙQv3h *34I3?hpfXM3}k2ed7]>S{ȟdl_vg5"sy{x9? @!*ÊU"-Q j_ Kڄ-o>k/+0>?37hA(ήz=gp]sp2‘ήG1΀/q,p0 ?pW.Xe.id081,WZ1]0sޥ1 36]`g,LoaX3-5s?f_dMӱ0,S ![5ި|2=x[0c,)MJ>AasO_|IӒۏqfGtS۝-:4y[$ y!I[Έ' 2wnYwc OaA>0k5bv~I,O 8?yŁ*:Lv;%;B9YeNqSIeI%7!s ʙv.wəPyR'zZN"f7jM1%ؚS,tz) endstream endobj 299 0 obj <> endobj 298 0 obj <> endobj 296 0 obj <> endobj 329 0 obj <> endobj 330 0 obj <>stream -%!PS-Adobe-3.0 +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 16.0 %%AI8_CreatorVersion: 16.0.0 %%For: (kalamun) () @@ -2829,7 +2829,7 @@ endstream endobj 331 0 obj <>stream %%EndData endstream endobj 332 0 obj <>stream -^ˉǕ6c,!j# -oV?޼K]~-L΢ +^ˉǕ6c,!j# -oV?޼K]~-L΢ (N ս<}V>k=\ʇ͎_>v^w{53jf9'9C +%|ßNb:_8Ju͗YғG]!^νaY_oڵ0k5٭\wfpzsn?Ns%Cr(Ľ6ZGl@KRӴ^]ed2g;‚´/آQ'nԠfdJ;郋ÂNCh>0$ҒX` 04g] nDž),poynB X h X\pܜfڟ㡣yoNP zofv7+OC-kH}tsqcU/טLZRYe1SQhT^ӲPVLܕj A?KL.V#5-N:Liݼt!ЉQ1TMYÚfʠVMױd]^{9s9%tK=H)aHyXP9?(.?l7;?+9lwR>%@tG^dm \Q|\hGebgfJk#ݕuz(;imlXiFh}sk+~jYvZ\zsָ*K~Rt#IJV<fE쬙61慏=^Yajk_IpyaL2Zd\hi=i` Zu$욕>EC}UԎnfsqYXSWŴ:84]?3tŻsX!7U &zM V=y[mq`!ڟKd{ZCĥKeŶ߈kw7O}m"V| mŪr/qOۏYi -RNnTS +RNnTS I\QKcmը"~!7} n Ba;[K}-B\Kƈp7ՠ \nF{RuKEqXf_73 Vy֎ZSIcCtjfd1py.G/f y63oVoO@NZ2gQS(?Q1`a4Tz9 q&8Bd@m0^vVeܺxS=?R QיYh^qR%^eml963i'gB>MvuL`9c9}TÍr?SPDO'[WKcef!U-(P_Ο?ȿƜ"w#xڵo7^`eީ'i)[jQ'e&%ϋq*Ǧ6ml|1uІx$)SN)&W̑I6&~й?n{'WU`ldKVf~6ƈ_#X@$@hcR6ާwh5O1swu_!K3]=Xr K1i> =3?1чz;øk%ƻ6&sk[^+}OͶ .Ls~/,>2K݆w `8wrf8 2Ž<(3A2_P>g8}ĭ `B`X߁R'o+ RbsA= p-<_1Py6lÊ2.ݬiF*Oàk^58{6)v4?:%H<= '#wq巀mgQT2^7a] `;: 0[Xe8Hɺg7j!xoq&+Up #$e-cՇ e@8M;.|9zxogXa(bL^BDzU{o @@ -2941,12 +2941,12 @@ o 𦰄`jap ;7"Q7D^cjhV3ٳ&7#W2Vądˬn&2@NgE{SG6A<:,u&Ch.0ݳ?q/n;^r7]&u-\:/5DQSձ$DÞ1ڎ&wL_[dwt b4Z۾מh(%D>O37F8P_ZõvKD>&EK79DutOqV %f3MifcPMt`%.>ɽ䎵S(DQO8 =~RY<1V!蛬{L;W\Y=\KRh8EL!giO1ݩmo|;xKWx^irzFy?&qx_i6щsŹ‘c`2rbs?yh@M-Z)!PHrӐDңrY}q)ۻJۥ絺mZq[;C[o6 kyKAoYb:`99>![k1߈%JR{Y|ȗ$nl:fNw`әx~m6s">k8?ȼ8lq^}~_Bx_I" Z,Km}|8.ω0mzfܗ%#j9 3[kZr7l~s'tz7җ[(/4-Cg4OICuB\́ 퐫}Jjf|٤fSGIP_YjW!1;K6{]dϳV>ℸFW=6߬NL嗴m/siFy'&ROZܙyycQK JF{E c 7G+p f;0*4#z_z7Spvt[#b/jrFqYK^)t|F˾2xvy+|G?qͣ,G$6:5}m8GimrUo [c,k[xkT\zt] }Xe܄4JmNwc<=VN'g c< N[^&.EUs臇wrEq {Y6b{n(>GaQOH_@!.}H7_]xB}]?u/ d UЎm&eduvRTS#!ØurKΰCvH2W quUsrbvfrRN q~R+LqyrϳɃ4:S#7{t7O^uev񻶪,hR(fvf6, h3vX"ޓɫ @@ -3079,7 +3079,7 @@ I dN.DI2i{mݫ7<ۧ6lzj ޝaBYNr=6Ǟ+f:j0hM0m{R66VF~1|Ys DvOFChP i/٭Y+狟s8ޮEmԆZ0n}Gv|mo?c1!xs!VŽDGQ3CGd Z ;4GF~~ENqwd9Z×wE ?~?XAHXDܑ_? "D//Pe 4K゙Oe?c!)6Xب^XW}b$˧-|\pNkM0"3ak+fBs2*+JzsZW2F-f-% gv?MFqLU_ƽ\.ӱy{NNk~=tgiss< -#2OvQIݚ9dwEs#ϒ|ovd K +L[y-و+;%ك;g~Sp&R6Z\bOV  :{/30'xBNt@:G= t=K<&]܊*٫`$ }c#z@uZ7"wbݚ9dwEs#ϒ|ovd K +L[y-و+;%ك;g~Sp&R6Z\bOV  :{/30'xBNt@:G= t=K<&]܊*٫`$ }c#z@uZ7"wbC5v[չ8H7Xv zBן-+~5T2Loxyi2}זU fn|ueN=oL).2=q>`k[>[/~ ٯ'fS_KOWAe #x3\4O( @@ -3189,7 +3189,7 @@ $KM )ׇi ;\#Umc\MXւ5'6JcF/R@CdD̘VO{bup)l(|oa8[hV6Eq9{V*n28"K ]%YX6y%7ر4K/ލ:^48u <as\݋ZyOpT|DRgOUDyfp+zclzPwnt>++5ـe;NYnҙ)>Ϡ+c/; =ª\]|Xh *.Gkxt{1zSTnz!*p+r!xP*ҼLUdL#+*2#"]aܹHLu*pM4qcg yj`rl +9Lq6ktPn7[MCh@ǫ 6^{{_o_RL ,1Ǘ.7~+{oF~Jѕc;hܱ׻wSzZT(prbXW~:W8I/0&sA}Hk{l$uIʽ#6 V1Vg48FHc B S@; ^  l9>u*pM4qcg yj`rl 68Wa}l:yjYc(c(@YJP-ѡN|:@[>~yT ZKaO Hjy]D^*ڕhKmNA!tڝB0 zOYb<ݪ%]Ftnxū@e*^6T.mW Zb$WӶbgD#Lm`-TIcI|B*xZt9[LvgKc X=wHzb,5E?ƢX"]:OٗJE5urum&_Zy(g߫f+Z_6!K- ۄ4B*o@  ;#+s @XD'F!blx&dy< wyiSbn_rO(vnA:MH`4S+AI-fXy$:X$飠nIe^ 21=FQ.ۈ1YR㕭iSՉAxnԉ0Gf!>H[/UKt4Vo1Zz==`_g*"v3&p@@?  7 eQ @@ -3200,7 +3200,7 @@ $KM d;D2rDa/TC EyW4([sa/OmsZP5TP1UD HSF8ɫJHYdOMHXz@P8:"[-SDZf*?{o%XNjce9뙬{t8镇PIq2x8g`m"˽ŝHdk$`WB@3:.Ef!mM Xf*Y \UaC) ժӝۛckYs:ͳJכ|W8<2ά9[>9ևm֡ mT"m}i{C̰o7VDHnK hJUrKZi- !&ЄM*UQAUH n@j5 -C,-Hj5Zk!hW| ~h WDRޡfg ~T&i.&iz"Zf&?7BE!w"U#)٭%SwQQIÉE}|:ٗPkx,Nʊ\Ƞ9^$MG.jLO[O&(Yⷣ2`ÑhZ[(k/f"8ЈN -? +C,-Hj5Zk!hW| ~h WDRޡfg ~T&i.&iz"Zf&?7BE!w"U#)٭%SwQQIÉE}|:ٗPkx,Nʊ\Ƞ9^$MG.jLO[O&(Yⷣ2`ÑhZ[(k/f"8ЈN -? \_]?/HLw;ZS3^,ƔCPQWN?T@635r,ޣv tX<4q 6Lلj>t(ݵNkbeM]L"YI>[dSfә׸\DCȼ  hv[JRd/JB:QLF}oFLKܨ$=|l6}> j•3Sk9EBO(Q#*isu`zP7`]"ƗP+D P킒1|1 zZ;8l7k{Kõ07^ҫvPj-#n5MkV"XIezמ79g%lpK tyOוvu7Nj٢i j \+M?o|Wg{304E,UFЮ!iNm\O$<=o fTͮGmvfH쁷Aʳ=6o~ܝ.Lr:T^J;\3%ܟUg Fn ήaQO!b8AIz/ɳH0N' ,`oBql>d4mzB A4# M->oE[kf㞊,PCM+tQ7vNp lV<#[@ oҢꁺ~"jnNp:Xj.ӸwqB*$7.hJ$nĨ *7 ZoI ݮt̜xhklӝk4V[9[VX=~Tk@]*7uw\h`of=#bQTm-l;ҭJ7owK7g -#Q䚥q5qoX.lt +#Q䚥q5qoX.lt VƞWڍWH7j)W!H7+!tn*(ߠwFoӡ\yيr560jW ]5+J-&t7$N]y<4 SvCVhΪnA vC<42[{|8{{Fg/U0Fɔ<49X Z<`k u8>pQ3w yY\ǧ5АPGBKhɆ䝏҇##lZO;2פ:AQ:';5 Mli ^:5(}ؙr`~+[ir4lҢSdex0,fA՝{>VzAgjrS1%=bfR%'$܆wn+S{=7w@k\EΔAZ$M ՚vuYb2duKo!Mobf{ȻІX=עpŻf-l9F 0Da(2{"AK|t |X>8OƆ.>q|o\p>i,E5p>i, ]|',YX>)84nx HKfދ:]}qS݊IJmÉЉ AkL'RHQê W/$tXB'ﲌ)f׻@o4p'Qy@ gi_M_}'\2`<w:K4NC!+7?YHA=Cona H4v3W]"MxJ~̽&7vmsnf=5;oLV2-?Mx wg.2@JCM|s 䒷ezm&HѤ s#$CӮkF= +w#U<@xT4 ad/^e yFy׹R:A!5PLp!ø^6xZˆRyQn͆$e醞]4гWVP6":v5;oj>"Go7@v)O؏DaYfƤߏMkp2Mt?6GIUH8o3"ޏL\W2qek7^& Z޴!WOjY-mv_M}ɰ*Р5YV YiC1|z;Qb/ɶ^m$zH%k݌ /@ܛD]PXCDO>DekAyo4HV:WyPܼP}à@Q2^88f()ڌbRvp~F @@ -3238,7 +3238,7 @@ vg6 wAQ͝>pu۽ ؇B!y ؇""ͳ>LyMe !u3>TS>xycm`K5q_C<> ` -u؇BI)PPj+s؇Cds> 'uwӞP}c+}^/L{-<=_ C[Pg؇q+P}>;ت}2eP}")#[mDfZ;k1f\Lߍm^\xO>I2YUW>0_چu).H MNɽ%qoNݺ*䮖6R̱5G]\ycMy6r vZҏad^}.}ܐ<;Trdf#Yo +u؇BI)PPj+s؇Cds> 'uwӞP}c+}^/L{-<=_ C[Pg؇q+P}>;ت}2eP}")#[mDfZ;k1f\Lߍm^\xO>I2YUW>0_چu).H MNɽ%qoNݺ*䮖6R̱5G]\ycMy6r vZҏad^}.}ܐ<;Trdf#Yo ﮴c1R]x.Ji1w`N2VnwW1B&ٷjL!CHn#R{DU U@0A>pɁcR}ǖ^y˛? 2:!Ja тbpk>1C=?!3^(Y;xEq[:QdE.[6W͖}qJ&J #8HF%Njm QT)2&7"08Tj߹P;K+avE.Fn dCe؊A@D?QD=+Ks"Fu,U𢨾1RN%t6*,RchxcԂ=j]?Qo +)1RyUQ `2ܟ+5L}׽`Ť˥*,k~`8|uR2)Ⱘܜ9|!ޭ0_o,֓iDVVT)sdL7)Ȟ"qF>] |3#dNnn>o;rh$%_wH<\k^$~C䉮.ؖM&^&JgHӷR[iՀsdxP^}wɻJ+?nf%[a&ٽl@fh %:`m*=F-,jNMK~t(ßPM oCuF3*S_jɄQJL;4)X lCȏH ]xBYsӎp<K]afKPj\Q2`Q'Z]$_C0pr7! s$0S-TeBK%%,V](qk4:G &Z^?xt)ŴQD:C`yå9jDX&3AcZk` ˞Z_}/64 +Ro5 &#bkL *7 +?nf%[a&ٽl@fh %:`m*=F-,jNMK~t(ßPM oCuF3*S_jɄQJL;4)X lCȏH ]xBYsӎp<K]afKPj\Q2`Q'Z]$_C0pr7! s$0S-TeBK%%,V](qk4:G &Z^?xt)ŴQD:C`yå9jDX&3AcZk` ˞Z_}/64 STq3VL%a&9HeYY#I?UOޥV_:S͕'äI]W)w3qg*A%G5w dEqnLgqh):ak*?tlgAxd {z ί:jp!x_s &edpk:Zd#2d1}>>v2cRGtd$zJE-:]HSbe :HiVL㚝xv3f`_:>xdYсp?8 E€0 HϠ6>4!mR=R< G@?![d9C].aT{#(焌dt>0{5$hC5_s/Dթݲck,9&ȃ6{hI?:rRT η9s@OcDƲ ?se({8HA0]ӧX RKD:1]{0f?1kz, (E8a{ .?badD"vN/< u$M@v^GgD~X/`Mpnmט4D96,g P\R/g*C!qȐD=c* ɤ(NF[Xred@r12.pu*lgs 5kƺ,v=nݦ()8K(RTg @@ -3503,7 +3503,7 @@ S) =37.Y [ !tĮE\hGY<2Q@o}L??`{]5 -(K|mtq⫣/b ~CAWo/R(B =˒8Fu7  fE&6C =Kxzo$yC!kb30%I.kr 'nhaJz ZDmdlYj-kڍ6bdٲȖuTk7 kAUe `x^T1[Pj^# m󇩯 sڍj9$ȬX'z?fxw *[n׵e?֫gצt5ֈrI~b냇  4u^!1qaA!B!aQAPИА؈Jq!a1AFFU  h|96$.""Z92$.:嘐(帐h˱a!11a!Q?6a~9,4$<<\ؘ8ߎ ;<2$4N-7 w}(ג2dxtHxtoGǪcã՗#B#C]_ӥS_Pν:GD_ _P]pUhWՏՋ^Ȑ؈p1qѮ5 5q\ld[DŅG/GD/\\HlTrTDHdl{;**$.2Q8V?%<ҵx1oGƩ:_ 0O-HºW)6$2.< Q_uMH𐸰ȰQ5&11aĵ%YFQS!qz,Ֆꮠ8 Fp/FDĆƹq!j_Vs-^X/EFzH}ޏEG05z72FM{Qaj$Ì]Y\Y0_. מ/.lWdHtDlUI~{'[5y#z_k{dͽAQjArm#jm~Y#"N7G0Q &Joa!j"ҿN61-ck\0DžIL퐩 a!32*#fK.eUnzY簠CSI}1zNF#.B_P_HxQeKhZ3jS2^ 77WןouZ z=֯G;6 zHZYUA :]%UX~HPu}U?o#w R=Hӯ#ou} 5GJn@? IAI?I8Cz> MXű ۺp6{g5V^$/IOɓ6->_g9?{hNztJ9&=c̟I)Bz[szvҽ&=S̟oIvEϤt詳I;}"-vDϤtOsI;ҽ!.vCdtsI  /vBdtsI ]} 1vAdtsI @@ -3604,7 +3604,7 @@ KEEUm \h4ql>̳ԄG<]Y77512iV^Q7`HSe>w~ ˤq#pGnm)yb䊰#`~_rvbn 觏Ikd sҒC|o;]v)ccMN1|;>E%e>+(.k|*X4Ņn֦G7,&Fo$=Ȟhz&"߾MA};.ھ4Vd)5=Y~Qiͭ/_|wH> r|$~*->KHRHm7s^ F}K;?Jeucn@,-)wiinhr3RGG  z$=3'n;ƚBFts3CTp^:풤"*k+X@o&WB̚'a>SVnanvQh /[7!h*K1QĊy,aRQUSWMEC=>"`L}/vIRޡiee ջ7Qo+΁!.,+[\vIz@ d.2MP aSZ\,;+#iړ')))O=M~ )-G54='@!n;#*͸D͇Ys֖b566557cD^ItH)PpDe-y|͍`&E;肋-Ijc+G =96ՍrE%0cxΗZ]qrŌ\ ԞpdL@C0kW^|A_W"FL>a-*ғcKZJ @@ -3728,13 +3728,13 @@ endstream endobj 337 0 obj <>stream Ѿ^,)=Ϯtwo~snֱߑwt7+}Ӎ^ʔ˽zN&4tԫ.~AO({]+鲳'wy/q;6vs9\lm ۸Mnov{=^5gY8g;φl:[mζggggy{_?G{r_^5cqq=nx6S3?iifNgtn::uzZכ/gŦٴ6f،il|zg]S{wD<|kǖ&)q븉xㅬbB).iHc:mV6yumХnnݦw|~};:u2[ qHC704Ͱ58.fl01/..:k+F\\\\E'W+jj.yĶrnRqS-SNx6[c]-?K!vL$C36Sn6LBo WlS۵Kڡ[!ݴBc3{{Q\)Ȏ #B 9\9}-/vluIϮaq/1JϸM@n!r% m[VĖă=~!-۶Oxuz^CF^S~mjc묺^OYnyM0nq. ThQĉ!&mwvx2i1ͭ6];h*m޴O.NUǴ}7=چv[6 I#lm ٛZi{=sknX"YC:/14Ӗ-q}lef aZuÉp8/ڝ]dUleah#0u6cD|{zCj6{u0E>xE^[[lyeZA!W %ፈU&^c^=WWE,"F/&.k/}xx:^WKz$7WhD;ih+b?#!_"HO.[{ǚO h%"l -->`+Fy{} RM#\ *43 ԉ,C#\䤭LZ0ԋ;ᘍps2kFzR's <"me"""YMs@kFas$1l 3L$v Žo:Ve] Гp ,%,!@>[dNmVXs*dG)yBKzԿ{i{# =nk:{ﻤG;`#j[թiZن¹Ⱥ×(],:LG5= z6m>,0{>ySߌt&9fXQoɼ<%8^_5=#鱫;Ws?c`YX\fvdp= - =N|װsB;eGwi*,6 .>lMn~-Q +->`+Fy{} RM#\ *43 ԉ,C#\䤭LZ0ԋ;ᘍps2kFzR's <"me"""YMs@kFas$1l 3L$v Žo:Ve] Гp ,%,!@>[dNmVXs*dG)yBKzԿ{i{# =nk:{ﻤG;`#j[թiZن¹Ⱥ×(],:LG5= z6m>,0{>ySߌt&9fXQoɼ<%8^_5=#鱫;Ws?c`YX\fvdp= + =N|װsB;eGwi*,6 .>lMn~-Q Ml紣l)\|N9]i˜)ېŃ)Sh[P.>pKP9"x"\A"9#xr-HpI+-/1gQP28vT)G-ܐ*/=ȵ2e:PWMP7y+rXᱲvعh2=ϻv{Nj^ m$##]vf3&|2g=#eю'Άr& ɑdGbCzv>B R`Ja"m>F3p-C/>?G3rWU6…_̯_(cfܟy~ON|f,3rkvcxtcZ/5m凸s-׃FDC^}645eTِz4YUb1KWtl}}0̈́97c0d.l{>!jDZx ,ztsANS wFFdvi`h4ҜU3o2*~f O8 a&K@y* {2ȿ4MHݯ=OV~5)9WCގCtCbPލ~ jq= W tٙ.q]w"ؚNx£/Crz[Sұ Q[{CHNHWD~C6;I!_d}UY]݁=]݂#]݃kNwܞ%ctԼv5'51jSPmqcX 8nR ujdڛ}!6f o60gb*p.JՕ+vq5GkA)f|"fظZLnuSp.,ŵ"̈́4yK̈́n&A|L$@z.g:dZːc0S:JS Si)]ssQE YKEwf$s;yKO(Uw3>38=o=P瑂S7 10~ >5$CoWm\".w0,=N,mťRzq)^\J/.s|_\J/.QJ>ʥ@ۏOx{sю̫r#8ÊC\b-Hsm];yQ33f]3Ņ6ʕгʥv!M܈ֺآgv7sZug`"gc|-2 Lam+n 2wm} &IiA +ftꜬ1R d`Bd +4&h,KR=Dx*W(GcFŻ>uSp.,ŵ"̈́4yK̈́n&A|L$@z.g:dZːc0S:JS Si)]ssQE YKEwf$s;yKO(Uw3>38=o=P瑂S7 10~ >5$CoWm\".w0,=N,mťRzq)^\J/.s|_\J/.QJ>ʥ@ۏOx{sю̫r#8ÊC\b-Hsm];yQ33f]3Ņ6ʕгʥv!M܈ֺآgv7sZug`"gc|-2 Lam+n 2wm} &IiA AAT,`FcL'qj0-45íi3P$=zy36XRXh ӭ35‹mڏyzڏrg𿺋4v'SLi~h؝z9H!{ }IJsTǐw>O6YMMVLVaf7յ=zJ_C"_-dNװ3'=;׾?=צ(;p~%~O |Nߟ~^*#W1ssے4gi 7zM;F/]krP @@ -3778,7 +3778,7 @@ oO MQ jqېq\5s\wOS6*\|Pc}4twwmL'ie'esbF2mfY̯||!(TˌGU5#SY~C-bV2o93|Mi5CosIp\}`v{~ 8f,,̉cEqҨG1_q+gEjʭNܪZ=:utOdͽN"ۍke'n63 m]c}/3;sS{ " Ӎq} PtvFn7jow]00Dy2QS6@x)r9h9rJC̝Bg#̽TBsC-$GsW5Z +:tKP`d?aFۆ%',Uf!\HYY2jӸ)c1<86,-xz(AЈ@5Z ;lސη`_`Wued-K!”jLJ*3b846gTS)C1웲N ZNS6Q~|Co9RooeGRϔ,; LIrPO/BFtzK4ͷ?N%}:D8A )b֠8b0ąx'R<L!Fh>Ze82EK-0LɣjgvmLhY{2GasH̵WKc66,ÔJGBBF>LKl!q9)+'c!ߓsmk*@ (B(c32~(YX 12e#Ve f,Aq0;ʗ;Jgo͛Gѿm}JY/D9vXYT dGAz ,xb$:4:m&KL9Eschb= LSNCCBe 6ĺ4hb?jhb,hb=gQ q41$ĀM 1D^v81FZo|'8Yma6'F,(A2/NlMl/O_ޛywЯ^! Kl %&śx=;@!WW7";fwwh4]XԌV3XmaS#2R?C]U9BnN_ɐkm/ˮxZNg=8]E9,$g2 ٗF #("Gu# @@ -3821,7 +3821,7 @@ a %JNz7Ad90R Xu])&$/4P@&|nc]3W8q֠\kT J(QS5YR(^C33k BT]JCϔ̆Sjƞzxf0ssTLQ>uɪLٶ2We(+ kfuۍ|[_O]G RoQ`jHQd^k jTChlirP"Ctz j3^FVJ+0qK_vS;Uz%Z .nGgj1>߀WhMݬr'&bN$8ha,ὃAqu2#x;W/  R KLZ?UuBf8#!"*0^V8[ JLmJgF[+U 'd2/TMbgg]{Ic@rwUM(!rl369EQпϾuZCs'@8z{j:Z."cЈ2&Ը2O?HaR.J)%eQFmvXq*,𕖫.˳>Zx'ϸ|@$ -Aur4=S$./?E .a#حY2UVӻ?G$ʓIb9q* C$$ +Aur4=S$./?E .a#حY2UVӻ?G$ʓIb9q* C$$ +3yE[2MfpάOaҼ6,f4跟D3u87ꥢXgU=;vh[vx۽4|[%+|#OEu1M9dHK&h6̃mNK$qv%+ |NZZt¬Kq]nQ1NFz-WOp5 | yj]*LPx皋RJf˨pC̀9[嗐Oh|ZyrWZ4?%]ˡC18*Lsʇ[ yM2^{|hjoWC^s%|5dn݀b݀b݀žtf}vf}vf}   000kK`6gK`4gG`v׎@v>찯a?mz!0;!0;!0;kC`؏h c_>>ž6f}6F}>aX#6=vЭN"tތ"t+Aa[% z6DH8yEV1@5ŴԠV>2"tKXЭWwg{z@CcפBLqPTTB8Sn=l*tk`S.,kP'0t3H/QN!:(~m{:|=F?I}`R]fl|#9In|yн Fe2tM岾 ~Xi5o8SFWWC+ƫUjtx]9\FWR+jHt*5bJRVѕPjtjth5Db"]e"]1Z JWLWVC+FUFP2ɣJ94XHֻJW굚2]j7ez"Vgۭ UӯK;^̯>ױX)Ph~-J - @@ -3874,7 +3874,7 @@ P_" ETGCE)ϩx\MW` u̿+8!/! Ncq4, P@ZIأ='@_El=}8}Ϫng7LHAވ#3x]+ jms*WU_*0@TL>cܧҨ -'BNU%ǷM`2/ 1??4s'FG;)[:%쁤4uH3VV[*S05Gnˑɠ՛\(-[1p- dDXѴ7 Pi!U %9zbYGL I$*mf\݋l8˞}zyGI}Hёh2Kd/ƊBv\ adM7W [ܧD5Q2Z[>!f5PO,ai3Bl +'BNU%ǷM`2/ 1??4s'FG;)[:%쁤4uH3VV[*S05Gnˑɠ՛\(-[1p- dDXѴ7 Pi!U %9zbYGL I$*mf\݋l8˞}zyGI}Hёh2Kd/ƊBv\ adM7W [ܧD5Q2Z[>!f5PO,ai3Bl aQf؍So`,K&2ޓݾF+G \BԷyC5|pbn~=Aݗ?{2d$i Q{IncCeo;žc᯲+L(4F>}dMQ=QۼHtiQ-az"(6˧gp[ĺRn8W oʾ4XE\7;{/,&FD?3Rr`UI_E {V^]e=efTHGU鶵U36#&+~f4F:̒5bULibihTHӻbz4{{\u6!9WRc2gUJyO[~RnUU t0Xz3UbG[6!TX~*'mG>W1*w}49Ö5w@jF- -s6GCcyxoJ U* a˫l/Ngz4QXgjpqZE{iJj|hְ.3/, @@ -4002,7 +4002,7 @@ w! K5gǗ­2J)|D vZ`Crv$/uũ.ߨr ;M_!u  roe-.NЊɅ#׏c,KVC:']fWfC'[oG/BVim^0 !20HY@>;p!T@AR3 z+)D S2u7]eˣkۘT:_11Z+-n3P, SswT 4j UR?eܡ?*eS ,=|]D|Z" m1Ɏ*WMPa Ǟ3 %H hkRp=\ފw9iGN%dhc]MB kEL |ܴԿ?&_*]Qt~ok԰FIzTDz9jwaz}X0% ̈́ND9EHj`X%8mf 29aPM0׫} ,?T 9ОɄA!>C~4EW%Gl]$!6)g'l!E{Iܔ0 Z^8z}aPOyܠ²cg .9 .*9a|}~/3/SV*ߘj-|?/Ȍ2Y)9g<~2_P蚽&_PGU1X[dϬ=k[u}Hq _\z{S؂ ege&\r4M4X &_0A{ɂHUy†\T5ZPcH A>Aׁ]*B#( ' ͩGY/ʃzYA)ԷS#X*$$#Xd>vNKʷGP!6p:B$jD=l }uxrd~quu(Di@kȁ Jr/n @@ -4098,7 +4098,7 @@ C *6 I2PWNG3Y:e6~0z,_4ٖ TC|ϱM>l>!<{a'd"ϖfpfzE{pEivHby;9>'K4b&SnrYA'Djq0 -]ޠ8B,p㣢iʌ }"n%4R%<ן!V{*D*u&}elo`N*j{x +]ޠ8B,p㣢iʌ }"n%4R%<ן!V{*D*u&}elo`N*j{x =+\۶@/@D3H\ cIIKEԄLVrk}dݓPLC7¢y\,#U=Su ip@) !J{뤊q ׻F LpkMuX;?jXaϕG<ο(maN zQ$>wĶ>HӖ6:7il[.S k#{j&PQ{$f%晠 nn8gM7:֊Q0wN7}լV-Yp¶*)e7ؔhZzߣDY\J>ꛡUnm?Bح!9 {gʿ 'a+Z՝rfܬlB @@ -4129,7 +4129,7 @@ d t"_X`ׯfS-A{k{*~c!hӦ)e%-;l?i5z lxW[+TpȃEXA8\ʬZ Ղ RPS\ZD,LmPUt49ԜĵzҭȱSI)vW^r0f'A)wO +j97(U x & 2 S5d 7`\ܠ78j 3uN`Y 9 1gJSI)vP ,JH e'o06N^eReN9ŒˠrIߕ:|2RphꠔmKAɌ:rmǤ0A݃:4 `!g^~>Z Ղ RPS\ZD,LmPUt49ԜĵzҭȱSI)vW^r0f'A)wO ѫlCJ`QO ? ;X4/ Tvp;Oi8RHyڤL|o"}R@Cd0iȳs[}AdžJr$C8(ueeQvgvA)ArA%AԉEFS`ml P8(wVT~{ș,失ψ8kP}W E}lA4@# +9CH:eP~NƠV}qdĠጾATJ϶0xnI%TnU#Ƨ  KP%K u1OIPjA ϭqi%?@(&HE.% RʎzAr 9 MN9l#rT{B GpuwY&|l,4FҦIJL1c &FPbJ&[f`Q썌`80(Z 3G>4 ^Nd5 `!# #hgAX="3S-oAq?(psP#(cڔPZft#ˠvΚH`w -€dr&E2(ZD "iԪLdvНB(tn[ɯbcqA`Mm$d}20 |_GY7,"'Mͽ#YWtvzCJ e HBye_ |&i$PC? $T1Ybxro@Y'8PF?N ({a@/`3Pg?'2EZy>]&b]m1,f58/ek/P~~;^_jݘrUk %(}x!Ѽ(F>qyw@)0EL ,V{Mb ][mW[}#^>m",#A {)5}$u#'C&Y/}(K8OZI\A}|p$_Zٿ[bU$+o*RB<}{+f݆9,Vv*8݇;߰lrQ>)2O=ө r8(ܷ+oAS[rO>X O܀ [H>.,>='C܇}(+O܇}@DOܧ*0* KB*r՜,yw*]W4}ρL4}lOP*0$rܚh )-Z%r<>* r_x#!zMr?hmN>ŇP;:E p#SVuqlYf{>ѯ?ˣȮͬ>d OJ }w&m O }E>+JN,p|I+늟4op~)3&ff;8}fL>P>VХ]ۇ_G)I'O zܾBW*_ۧ Px}SX}C h ˖+Og 4>!>Lb[J>d؇+"}RfUʄlh>ZgW b  bTR";OYOJ^ &Ɔԙ}SH̾Ev P-;O?%K`@pr۴ʴ7"`]>OgL mcc6}Dek!G1$ρBu^2> @<v.DX'G5 rs ܧ5cv8} >K( fY'oL8Cކ3K(vI#owO"QTQԧ(P p:~?addpp#O[&{g]<#96ox4kR<|֞Ĉ붥r"B=6 - ,`W(_hS! ]O+R<9y|2Cx|S}ES4ZƧFZ,>%dw>@|w >o> + ,`W(_hS! ]O+R<9y|2Cx|S}ES4ZƧFZ,>%dw>@|w >o> "G'|[YowBùG )|Oy 'kP+zt[6S[1ԽwY '^9 5<po* gb9SGOճ%ޞvW5 &m.W w4{P/c]_ISbD=ma;uYת'){7V| BBM`OXj_O`ۯDC ד<l=oh=z[BMձztfY M-UPs3'] SaՓSw[J*5z,NY.&H~ʁ+i&@!&Գ~N8SO4 ;Q@TzCV@ʋvδglGo5.‰z*߈z&mL9CFΎJD=fC!$1@oB+'{o@=XAP zFG+PO qGg"AK @@ -4153,7 +4153,7 @@ r_ C6}\߶bLPBןL^V\"M%AL,z$RAPrWdoP=)֎ۡzTO'NCvP QMaQt/q ae 1z$WɏzLt*z}ӐzSHHE$%e;)OAOB[slTZԓۘBԓ|G7g2`K\G̞oIR, z:;ztS侶gq~<vV<"uc;cclXnH=Ӟm$vm !Q- םz#̩]$=oovAZ~_I/iǡP MkA!T{=i[fz!h]=qh nC/z:a6ӡ'շݸXMͺ)1e/ntjoKW9ЫsyCA*|CaflS+Dwv׷;'5Nq̱|h1t:۩tk{2zGm+%& +0wH(d#'%Pϔ{GGC T yӆPϺ[#QE$%QC4=JjH,=u (󬃏>m$vm !Q- םz#̩]$=oovAZ~_I/iǡP MkA!T{=i[fz!h]=qh nC/z:a6ӡ'շݸXMͺ)1e/ntjoKW9ЫsyCA*|CaflS+Dwv׷;'5Nq̱|h1t:۩tk{2zGm+%& y9k>?[εU΍綞AiwI:L|]뱏J~js}^-v| l VgM^smC'[}Nµt)fA-Ý^ML{8@5>dQعW76A%[Ntx)?&z/yvNﶶۦg\kNwT{rKwoJ3 ݿ#zΣ휯;Ž|tf9S6]_m1 57ňsw댫QK拍;6Z۷s[Lj!\cRWMG'>41<ݺO6Monn~@e BO1ҫ6>3iOW˝!z-Ύ1ʫ+]i6,-n 0G>ƻIpy~OEλY>S]x6nϧk\><7֕k;ќN/sϵ8qz a.O@|<~7!^/D5vPg$>0Nk}]=F2j~  ﶫ(AL⸶Vۼ0Rc<=nr{=/ 9o~+6>!]:p3zbV]x8o"'Su%v|mAXٌC"!3Ķ=&{(eXj+c٭vOUI_秾}脉^ WRs|5K nknLboN3 @@ -4172,7 +4172,7 @@ u') ܳkf0RaRTM!=Vsjj79M!$ɉaz rbᕊ6Wَ5u^^w**kp&G7ҜIoBhк .ʤQn g+R7*ׇX^18^wZ5#:.fN čE\M1V8TI\PHbͅZ4ƉF5U>,^ -sIuǘ=#iPJzw -g&eeCSSu>mzVFS*8=YWSOSUq1feǜ^ '3sP'f%+awa}(1$'TPGXX+2aI5n!7.[Tۇ%YzSld ];@5K=cMDG'bi],Ac<Z,e5jf~٪QLVcO _N 4?!#jM|a+~uOINci|Jr)ض Qأ#EƪO\JRN.YRh@;mG]TuDn$rGԢ:vqnf.v[$ߘ^3P -?Qٶ^!L\[/sk8rA\G<-$:>L.D20' kpz fk +?Qٶ^!L\[/sk8rA\G<-$:>L.D20' kpz fk ksKim"GiVc܅5 ̣4"O+kɷY dM |\(-vY3 0з(JSh ߱.qI[Gα[C2s٧mo5?Vp|WTN]qg[k<@m\hvՈEe"`$?PN r{lGgThWʰ]B Rk6~TmykLtp1TM3JѥaxFÚRr7>25O6;OjYVæcq]ȳ<kTUa%3r0ޯih|PoqnY1wkx[4wuġbZeok@/kuMUVMq-Iȶ,!tk}[$W 4j"Ք)_@@ow=!Q%$BY9 i!@8 kO =(bcMsR,\2xAY\{6u:hChdu!6[ !je#}FUsȫZc r]#}X϶[wo@ui´ (E/F&kوڮ*'u>iɰU`U-je'CPx5Tz'mS]C1Pذ>)&pR}t~kh.G2j(G~~`h2t+gy%-n jd½fe/>NEWq: 1CMڈcR4Bqe`@UriNK`_1v#x& #g0 :w7h{)_c+%KGDzkShUMGODy27 o" 7rw虃xSE{i~vEh0R\WelЃOtT;bHvR2hRل;69rBǕ"kDyUUI@LWw %3Fkʰphg8ˉQ(a)JM!w J M F^̎eǮưi]_RȜ.MKq](qRHF.&kJ,N @@ -4192,10 +4192,10 @@ Y4 +HMBCi Tq>5U|$M5Z&9 N bPDY6INs_:e%JCs\9k~u 4= [Z7C51 T&B-Ek&զD4 %M,RjR ΋A|HU35CɞGoɍgPCᣐaAfmQy*JXA ^/jPQu -j>"JDd =$im5#X㰝Ϛ#N]ZB#N^4>!y!]-'{/Rp]*<rQiMRʵRWVy&q c~X-B'I;*FX '诖Vˍ}R@0: :L IҊdkaj \kҚ?T^BjsJU,ZI(O2 +j>"JDd =$im5#X㰝Ϛ#N]ZB#N^4>!y!]-'{/Rp]*<rQiMRʵRWVy&q c~X-B'I;*FX '诖Vˍ}R@0: :L IҊdkaj \kҚ?T^BjsJU,ZI(O2 \H@l4 JX BD!EFWQ5Y`GHX }8|͇ɍ)٘DZm[)V:IE}Q˜66JZL;pdaښ#Pۋ]aO*k7Y5Hdiۚ7%aSW-duH-,J7A]+ TV"xOieKS%=QPXhC_8S+W 3TLJp$Bn,DFPo\gW#><3,FcL$x|A1\DHNLvd?Ǡg*RҮo:zxY:mEsrɊ6u52i_&./+S#(Y[\#җ[-+s|*Δ8ti7DO!x'kqZ驣ؑ]pp,%3fT1_XLlJ6#w/r!|pl={f\Yx֘Pb |LzAn?$D8Ōqv|Y*̥|}Fd*T6K[riC9%VʸP`Xcy47 S9SvzP !gJhIǔ5Vu99*~ ((k(Fq=mV=V\D-r,KÆX.C*haؐ]Ct7P,фB怎DmKtOAw0BN;-,ΰBMbe"_΁`\EGOBh˝AYmĿLIE$lhs!~|8FPbK$ H ZC+C13A9lLI!KN@YhFf^,iy#\/r#i=7ƻ%^|ʂq#M`Dž8c!foۺ]pNiqrdI6\pB0#rEjR {4̅ผV YB/V˝aDAVK!vo"c.9_ ᠾ|k `dM#0K"& *AWDʶg/$9x;R-#֓ +N/g1r0eGLuc,4.r'}̠e%. zN 8Xǐ蘰2vLϼ')UBΦn(ck + n;YOJJ~/`#a~*% ܔ('M6ld|pN&`3bαuˍZnlłn>7P,фB怎DmKtOAw0BN;-,ΰBMbe"_΁`\EGOBh˝AYmĿLIE$lhs!~|8FPbK$ H ZC+C13A9lLI!KN@YhFf^,iy#\/r#i=7ƻ%^|ʂq#M`Dž8c!foۺ]pNiqrdI6\pB0#rEjR {4̅ผV YB/V˝aDAVK!vo"c.9_ ᠾ|k `dM#0K"& *AWDʶg/$9x;R-#֓ G$L39@)6FIf%|]ζ!5,fӜcl Z-ѶBi u!VPre\=NțG٤ a`pa.tYc9k/n,6 0EyH+,r DJ @R.cc]vwloIE@Ŵ#K tmeja>V. !y`P~̊?k׎4~S1K) eFXviZbT?Lc1jfK>g|H:\\&O*" n;u|\NS6SPp!5 *$:Z s'.-QȜ[ۓĮ2ݯ H#_<rҨvD6'~^O~l!m!6_  Ց_ IGBC58f]UYO @@ -4238,7 +4238,7 @@ N!?A Rbs-yO2*Cxc9nnj#bw} upO{)>:1ð|R2.I Bᤇ 1 $33rW) ^ FE! dX|{9sx+g^ {z.Q$HiT}@N7bǗ1$2/H8Z˅7)]pͲ`1oV1o#=6'RhPADIh;bOxK^!ITsp]L Mb'o`DрmzcH#'v~<x7øz&ӣ+)(ln\ؖQȞ"ss#kq^xhօCKa $l߿4/nP.ȘT) -/9a*w=vʅM`*GEk 8UP}0,*Mkh 9'xJTWfZAQΉO4/O5<]}ׁN/sorL_վx~` g -@D3>7#=dlܣ:hp2b}U3Ruxd1at禨uR˪UKAGlZi]F෩`z?O̅/ bVڽ; Oߊz'_yyd{t۱滈>>b׈o[ +@D3>7#=dlܣ:hp2b}U3Ruxd1at禨uR˪UKAGlZi]F෩`z?O̅/ bVڽ; Oߊz'_yyd{t۱滈>>b׈o[ 8 \`7LU$nNae* `PjDs!%('T *Nct*V-S>uv]:Љ8%qImW4PZM{úr?V[˼0:.SۑYLV{罼"^AeW9!Afx\r]-A?Xʇh2GR0Kss8*JCD7bd?J/0qYΫY<׮fE ͢햇9Q^5Y¾%}.>ꨭ7,< | WоrzP! anYU}RYB`Y*Mv.?e*~Y(%o~^S{PL~7-P0523,7+ē+g{﹍sN`|vmEm\C =q_DrMp;ePR?'ua J_b4kg[782_1PPg}V۫@nM7?)mJEzعX ̭k=wUMgB9WC-DbKijűoՏ[XJKij~g i';l <˱> 2>+" N$+pۓYVxh gP-78Nq~&(Ycބ~pEzLܞ^-j㰖Rm h7&F{KDg =Ix*^[U0;LrAEhߕtE7P9z"u9a{ӒuŝgOxW4(IA%SQ>$)l* ]+|_ @@ -4690,7 +4690,7 @@ UlS@k VKs2B VjNg @RժFIvHxDmwE a/L$E0Wq'9);~R.T<ʍ\>8 Œ3n0"Pa`&)C2[c".Ur/UԬ(ۨy97ydϧ 4{ofh{5.Dcv%GЎ&xf1K% !q% H{,n$Еlz1:ԂI֍h k\3YP|j0q8x7P9l AS':e ɂdQ}KM:Hq.zr Xr̠㧻QCGwl5jJ AViM: ~ޅ}1d"ǵ\f3?,j~,#6Ҭɧ$;R=N.?C;BVeX1t7ӕR\Db_䪥ypjGg?L{3tq 6 -fB"]SBꗥRkwMXB|B_/.ks.jd0~bQXKQ(fG*^u\ju'39oc)_p)x*Yh +fB"]SBꗥRkwMXB|B_/.ks.jd0~bQXKQ(fG*^u\ju'39oc)_p)x*Yh PV)-j(+mP#ͺzgݑS"H%~&Bј3LwޒK'EFq}`B>qR4 e7EJV1Y,G| Ks5#i}ALo'c2mxYmrhsV7ՐBXhk,,䂕ȟ[D'׍p n" BgQL3(.̺a[+L cAGKN`% ARY@M혌:ꛕ6Q;tOjth_`tZ @@ -4753,7 +4753,7 @@ w ^f`2i: 0y8jJuxN#_$F`w`pT_o|fЋZʘ6iRli#q\1:yQK>qKzfwl9f\۹|9Іi81р" }s*۾6M?km<7z%Zl̶isz1UڻlSﱯW5%7>DF~Kw}oKj'Yu.CT$zE;{1RJ􎫉 +M"U=%֬z{ - ^4(jOb#"6>$zE;{1RJ􎫉 doHU/,ѳh̪[OB3QV&6?GP4D*_y}!L^}a\2(DWHtf"vO^D,YPW/ FP]j޷RSx໺;{KC޷$ͭĚ\4럈` nBdD:(ANg&HDtɸOD7)>zR3zLͣz|ڵ9{{Ŀ2m1R4dJX |ʀC\/2ݛe ㅁ5O;n\5:ȩ٨P#r3l3?K۩>w#R*ǿHB= a W+@ N+ި١_~EOY 71k+7>rVҥ^N;nQksO{^6$+X~?;hb>+Ƭ-_<K|%q@ӂ7,ky|'#xLifkcrX}> zOfv[ytʟx4> tA_Dj0L~;@1vt ^R}T$X Ⱥw{d.gerc*ݤ6Ik37t<31" ɵ(pSsoܱ0Xwv\rsn6>Z7tס=+ak#:#h~ P:ݏ R 8C1`|}{{,\nȶCp"m)v$AF<*1X=R_H6w@9{Pc-GtX\ O~Q}wNU2|qI< tn:Zaejky]} ̧N6HP\"==$Ġܺ7^6db,U˼bdAݖnJODҠ pb^Ur ϲj/ 774o&OJ{tզaao3E{H  ^mw~+c ӊ杓/`.4┴Ͱhp2Qp6Pw>)OQ=EE[;Ic&~& f m>If(+D @uQu܅ZxxY^ĵ~ZpllYJ)cs~~^³=0Y>7>Vw"ר(bHvaGÝOG F=%,Bn6>8H̍mϞd\17&J';Uw%@Z< Y9DkaNBMREd47FRz[@ξhibR1Y)V?H [d@𡾯na`vrEPJ-X{ʛ{jY,TsӼ,t]ļ78V TFu}δgOuݺ'@Y&`4LtulxZ`zUud7z7?rfMUKU.T45hNleU)cz+3L#` \.Gi*WX3tW(P?Q$zk+,{(9u-N^Sg,/kHEd0۩ KL`X g]8I '(ܵ^냜8G6OFlz9)\XʞpA|Un/KuJ?D2MɬMfw4U*cevgtqXdt2YK6&1~CͤHԟHt}wm9͜;X\zem7YQ[OXoPIk i[9]iQ?LFfV*r;`щ__lylCZ^ ~0D+u)DҴ t{w48/z} -5\ZZEkёxFBI-|z 1߻2W:N`Rpwuz5C;*Y\RCvf_Y~)~)LO +e\:&#%΄KJX8fs)dхޔzyӤx4_O7k7/?N%o}2:hw( YmA +5\ZZEkёxFBI-|z 1߻2W:N`Rpwuz5C;*Y\RCvf_Y~)~)LO +e\:&#%΄KJX8fs)dхޔzyӤx4_O7k7/?N%o}2:hw( YmA i_.&Tj=E^ҭ$fI"{M8zMoB Fw%va[}Q "d&(̀yS%@499O'=7fEOzp'3;vQGA&#tn廽\O iezaR#?&KYug/ea95Ɋ)'EF+bSe282?kPZ~ԽuJ}ޓH3r8qLvndnAoD צDq^Oz뱾M Ӵ̼sWbqp{Wg\u7Cj4M~g.?9kqA5~cH>ݬ ڝѧʶ9RZڴ&x=jy  *8ޯ8Ҳnbw Vl O\ckwᨍ~qKՙm1pG% .YϭMhR+IzQwufʷl:zQT<ʄ49D܅DD57}Uv_ Vj ;]ɦ6q.QwQh\ǭƌY@]vT}}rT~ɏ6j"|ɽUa3mm754`;apafhlF۹JwVھM >iϖ>8FI'iza%`_KEh{+hæ<1=y[˖$O6'1\?-4L/AخS gȳ^^wOQ;yW(7 36c'r?*8CݥD09t=]uڴDS. @@ -4871,7 +4871,7 @@ K: 5J;rOZ PwuBu썢_ncm9ܾ5 k6){H5sO{ /xȏoJ{\Ã'B 6w4 *qA_c7ؤѦ׍l2ַpUv5 @ߝr3V[8xy{¹[ys+44)[P2qfalHSgPX{̌#ԞQ;ūF<'5K>+⺜qɝjmZ\DBfXq|a!zqIA?ݍIqWW~f;[u򫢬tKր?U uhB~A o4"fu2V7tj˝9ϫ:ߦQ\ʝr ~z*pEM` M^aSWPʸ2!ܵg#;]UjN$M^]CV~j> P -rkn5ifK޼|>8qupzrFȜ>ȢT$REAf)!?\`2_9),왶\{|"=L5eQ0y~\LRlYi%ܛ=f/"N @O̸l8)H&U`rU2NZ'K{`bCmPIf y#b6_]/wr hn,Fv.)ر .(Ӡw{b +rkn5ifK޼|>8qupzrFȜ>ȢT$REAf)!?\`2_9),왶\{|"=L5eQ0y~\LRlYi%ܛ=f/"N @O̸l8)H&U`rU2NZ'K{`bCmPIf y#b6_]/wr hn,Fv.)ر .(Ӡw{b ! E^Vpo_4IE-IHJ$P3$!Q܎RƬĒ,+V WxOtZ{<sս:{S>uk.D_J]=ݨI$I 5(Msɢ2FNp6|љyڇ4у;?{ &w.iٽ)iask94$.׮˼.ViVr<ٝSE<᧫/EY闇~MNvU$ )gϒep;֚G^t wLH\[ :OWq̛ҁ~R>f\Np[8$+eV?2{P+ YWuE]ҹ @@ -4960,7 +4960,7 @@ Hk W)>SᏞ~  IqGx}G|C[>z^|Jt{t7سAvnYcHrQa|Y:l'ዴwlOZan8u?\M1z<t9)bSdݧ2^JwvW}mln-M+*^7[YRKv sJ5{O=_kԦRωQ+nV=h~r;^xoW}>:87Gk7prU[ī>;-eo`sTlSʇ3bOcOæu@G;{׽}_n m9ɮnլ|ցŅy|ΑŏʟS3B'`ei\u4=l;y'vj/9P??%V?~nk>2р0X>k<_sGw:ۘw6+:v9N|GTyD)2!ÉCS Qxw}oϞGx7nRNm]lc:?hUG}ym-CRE&-tG}ώ~nh *o'KHmw_d7un*\r*w.ue9$-Vg:͉?G.1ShiӸXv 35'"}\ٞ۔fu0 sp󪓴xڷ~V{9ZFÛE*4"g4;1:z,E/>qr>qZܯŹk59˄󼦸}%  G$:I۷.]Cg5E4{8l8Z@Ѽg귴/O>65r^+>ՋF{T^~_2ŊFg}u8g;^~\Z'H W]>:ҢnhNޘm:ZY~u}*s]c\ks#OQ몬~h -rGfh^)O[gtvR!Kn:kMs&q.Z!ǦO'ۜ}ߒ_iu_[AqY^W,Uۨlnѵyl5_U1)b> ̾ji; ϵ;-  Sο[@Z 4ң*w."NNܗ\\8^VήaӞjbb4ovQ⑪ٟT-W`4;c @@ -5043,7 +5043,7 @@ H c .(f'7d;YzٚO_or'#}t\SA!ܒX9^.prv/@fXߠO6B&:’ |vHT2]z:=' g-%ϸ&Q1ƊV =G*k$VX*˻ З!X?.]>9:f$?3U`U!yApf['9Y)wΙv.e^zڱDvfc[w7qg 1=4{]JG}D.o}y,phRry"8܆{}R&PoAʊ6 XmzzCKUw~%sdh9A^;^>ҾQ€~RED;NgI||%odS/|O>_Qɋ{ %WŬ0/φ7b/8N=oCCSc uLWdyDM - -DcDDg ʗ)ҕȜy1!LÅׇ$Xcxԭ] Tf!)K 5 H"Y@ W[ k*H}I䴫VYe7 ( }QG + -DcDDg ʗ)ҕȜy1!LÅׇ$Xcxԭ] Tf!)K 5 H"Y@ W[ k*H}I䴫VYe7 ( }QG e.1'% S,+ ! OO7 ~f&S=G @=l"POHe-ԩjE\[ ծR* ᣼=ޮYih pp|枘2 |F5P hfN" @@ -5268,7 +5268,7 @@ Q Sm@)-oRfB;5B,3nNbKB\ƭ~WA{+U%lUY5dtdh4Tg}H e8vcR-$sd0wsK6JstP},5*KKC}G>- ,A_pAߪeួrFvO=&C{nk~m;Z8vٍ:A b?P)s'=-Gk6 5CG}u%X,vo`yƺW;cuEfa#a8AցAֲ7{"|6 O]>#KI2w鰸嵢)]kk,fȴGU -8#_f8X +8#_f8X g7,G S=K.MH6\nðMi: uw#wg2vԠ dlZ6xsmxM @@ -5280,7 +5280,7 @@ V zYTz( )V[;uz5,9lLAo}Kڲ滭$LktR / Wc(8ԧw\a'KC鍄=+xTy_ ->z3<+oug# qgqyx,GsU|?&6k՗m[PqlAn\wb?'WDQ4To ׬:U/\Ilkn6]&ji$|x +>z3<+oug# qgqyx,GsU|?&6k՗m[PqlAn\wb?'WDQ4To ׬:U/\Ilkn6]&ji$|x wc[o59ZcQ].Zy;'SrjGғ'ʃV&A޳\g*]ϟECfoLTqFi|p7aWo`N+paSkTsL@킃au拒KZribbP,RB'S3~TͲQN*Snf3|FaKj C6F[}nt*un1MR$m91 ^8;X@(% @ @6llcǨs KY[Dgu6<\TCQOaj&Y'yI @#߫63(;aϨa5ˤuV;_~bpO~8t))EI9qSCK㘩R?C{go7YMwT'u+D `SqqĴDKiynuI,)r#_1}Ixn۫4f#! WLKq#uɧUn׷S=eM+P[?#Kj\v)>={rVVė =D}`њ6w;%'SY_dz7B}Z~Z>Ek Vuma!؛/¿~}ٵIn'Wth_Vq; ~s=Xc.(nti?1-8jz\~}RؽZ΢oXrKW7PZRqZ徇"|ր؏3.3)=vbQ5zܺ_Y4Mz_9yڻǻ }W.@Et3+Վ夕X&?5ݤg-6nQ=-v= 4e wj09mtX >ښڌ+ޤӺBUN*U]<-πw%pK \ދ 0| ^wM=D.7E#B-])=HڒՏhsp[m%TsK5+ko-?5Geєck<(9yīMaw:#kCO~ x) `]W 78sw#,o(Oޚ.ޖ&nzRb^ӦT%r_m%܄!;pk-FX\{s~yv]2 "ilM\J[i H\q71q6h358ퟦM(kj8T7QT;n(p{d˶2:* Kys'γ{0̘U(f[9 fdaटQ5ONw- ϵ{uVObI YP?v^oG%B~%Vͼ,)[%WH;YF(IT81,}c[a[,3.`GFϹo6p"FB3?_cz 61~}Ԙ+4C,mZ;?n[-(j(D%"=ù̧.\.WkV';FS[e7@9dAiq vˍ|z832*62ƅ~U>.q@ z6Hz G3>sx3O~ӯbłohȧ^ȏ"ei@sv-W^9Hh?#Ϋ=ҧIn|ܿXW7ֲ__5uEW;.I-M!=g50/GVTc|boA:`|ɜv *jsM9xί.j"j/"rTm_IFK 0l.+2L[Uv#iĜ"Wu|sc~\J7ߟϘI0rYnzz -7By ß1y3Q{FFA?'m]ϝw/!4z3)S LhL?Z[=tY]<{G>7йI>N{n_j\/hV~NVZ襏kuPgyY`ڒ_&7bNae6et81 ߈WȖy\xN˩E +7By ß1y3Q{FFA?'m]ϝw/!4z3)S LhL?Z[=tY]<{G>7йI>N{n_j\/hV~NVZ襏kuPgyY`ڒ_&7bNae6et81 ߈WȖy\xN˩E amک 2U{Ib+C{t+}3eON*%7;1YXYf,1m3J}ض^]Iح-AC~0~eZLrrE;}2xpAU[{i;toB:B'N琥䑁V$H{.mUjs9R뭕QԨQ$_OF1EMi ,̾_2ǬT*gQ q@+bk.0m0Vo;V/c=q n1XlTWtPsUT~jf-ӂ14FwZ5 qmm"QSx_잌 X/j7mr@M ^SȺ)[PDLἍYmOgtdUAjīai[|4LD YGcy%^FGe˩rE r^ZwY'K+ vS :v7Agݕjg*DQ,abC{9i|aldT,{4++~kZ6KCg62%z݂,6=A&?(p[džf;0j+g1FQg6[g+ŋ?MeAP-+Wɷ\??*I]lrnUyH9_I(~8'Y1-BZI5y<9OskZ8-ʺ1J?XoJPc;e{~yZ+ETNH[>7y:3 @@ -5372,7 +5372,7 @@ r d#? ?tҹ2`tj~oW<8%L[fY8tm^ .? dt&h{i IkȖ5T)>kXVK#wUk. ,%-9`ec/cٌ  ճ:hle9~-~Z:5߲2jqNj{UMuYm*cXiUfo^~|MmWł4[$3ыU.亡.W) \ɣ@PH^4s:ݛz[؃{z`Re,^ EZUӻVxu}W\:@Jߞ4N8ߖ轥@;@(':n*U}w=l8g2ܒ0 `b#:ѡJ3:vBV-G9=鳨N%Wl$?)B/BTr?8Un$hY+1J883ѨhqX1=:+iŝ6S\.ޙu?<ѡ\m]&Byɭ}Q`iG j.8kuӣ<*ǠuZb.-(I@@&zH7z#?ylsAdz+f7ޚ܃G}F0˒;ER y]^?.&@+lYl'F]>4~&W[4)/xĒu#C2d%rQL3N0vG0r7ա -Mє-8ʊ]2׬1-<37Ö*Ѕzw50trR˃Fz4ȮPÖ́XFwQ(|Ӎ^zbEW#ģر\Έ.̵zP+iJGU *uri^[j(4+= >'Q;ئ5o?iL1>©zKsgGL4k6ِLar}len?{K~ ۃqSX*EEcmG>?"Uf-\pNXϰZS@abdM]0ͰU3c_Onwک͕jEH u[|y/ʓq3KL;V9YJ16ҩ%b#:c/epVFb\\(ctxp&9%E?UTiPFL+b6˯/dH`BXhFzsPuɩ2\Ԭir{}| +Mє-8ʊ]2׬1-<37Ö*Ѕzw50trR˃Fz4ȮPÖ́XFwQ(|Ӎ^zbEW#ģر\Έ.̵zP+iJGU *uri^[j(4+= >'Q;ئ5o?iL1>©zKsgGL4k6ِLar}len?{K~ ۃqSX*EEcmG>?"Uf-\pNXϰZS@abdM]0ͰU3c_Onwک͕jEH u[|y/ʓq3KL;V9YJ16ҩ%b#:c/epVFb\\(ctxp&9%E?UTiPFL+b6˯/dH`BXhFzsPuɩ2\Ԭir{}| }TP1`c;Ӕyk<c9@a64@eI*. żb C66+@(|AhVT1n0UbK8$tj6jCOT@z<|/*EzqnGɟMs^+ߋyA׻c`تl7vvOҿ.W~9Lد ߵ˷m2z5C#R{ao+BSLcľ{g:_e1p $T WR#zX&j8jrT`㏾6%+!EI#B3= Z֡ zA8];"Q@Q3~&lnH>́d;ׅy5 }6/I&2`z~ Iے+<'q1H'DM'`@F6uցC17 ? ,|(= N#@!((i{k[םaL3,/Eb|v) GK mVv؍SșH߿$!GE@5?GS(& hvG5^@:uЉ tPhOȫ۴[3P)uRۄtMۑh}IpbM$]+z&Bd>0~JSfwDA^٨Dl34r7$ٷWo[a~es_dg=D'BWN/=Dx@aع{6{X+rv:Kn$j_ WI5O¶hUdfOsR9=|v6-wJ+KqIgaO\pNe|QS~{!whlsz'3N\3_~4ԯɧ?*9 @@ -5407,7 +5407,7 @@ Id lkBqŊܒxzrjG]E{-օ'!b9tXIڀs.J>zi-ˀ#7g -|oD# M$ a˹㌞Ѐzs.ĵ~ƟWIʡS2ߚ:Y%'WQ@l .ny .k>#20z@vz@ j| :0Fvz-pd&mEeYݝ7Sqj7s4A%Roße 9`X-p\9sO^g6{SaT@%rKFQ%_%<;Д~yοcWVj;Ns{S{SI iĿ7]Q.]GNj@3 .? a m@- h]BZoat-,U.Or\~ۯ2֋: mtw}aD<=7㸽Dʭ,Ȇ.{)8oI_@C=plk%ZlWar΢!{x.GFmElw[)D8it m?.0j3 wܛQv y,b&14 %|뒽(")̮GCGYFѓo7quU+sctoby; jwtFtѮ\Zx Ӿ?w=mpǡ[&ʧѱ-hFԢD}u>tAi^~>N&FqtHxY\p7@<VGvnTMk}'n5)4:-h Q1kp]4:}B)tDO|Gv5{ՠ`*beUQ+=_oʲLBަp]X&9vӜ}AګUՉھUfj\\SR=Q׫q*Nr+ZܡL˓+C4ofzNH=܌8HjNNr@3%{R=zNmGիqU5܇EE}MgB< !S{0W""fJ{/T1 +{!E)MtZ`PS3|hܪgr=OيzZ*jK[.ddsu\keuV!EC>\~ۯ2֋: mtw}aD<=7㸽Dʭ,Ȇ.{)8oI_@C=plk%ZlWar΢!{x.GFmElw[)D8it m?.0j3 wܛQv y,b&14 %|뒽(")̮GCGYFѓo7quU+sctoby; jwtFtѮ\Zx Ӿ?w=mpǡ[&ʧѱ-hFԢD}u>tAi^~>N&FqtHxY\p7@<VGvnTMk}'n5)4:-h Q1kp]4:}B)tDO|Gv5{ՠ`*beUQ+=_oʲLBަp]X&9vӜ}AګUՉھUfj\\SR=Q׫q*Nr+ZܡL˓+C4ofzNH=܌8HjNNr@3%{R=zNmGիqU5܇EE}MgB< !S{0W""fJ{/T1 kQ 8/X>^~FWRd>ZD&8mC &w~(W_.LZ}ܒC=='?M }O[fج/S*|`w2yt2YQ,2'xf$/6FНC;&IA]Soыm}|K&8e]A#2 \xS{- KP7VeJd~9i$ \7e8ԣtWg2+E/#V>S4un`soP̲2AtU#dkFC=jI$D"W9mM>&t+#u2m=Z}ݵIvsך쳌xj)4vh_5Url[EG]~">Er_oG %E+.A}Z.JwSkltեŶZ[0gMWfMM?1}'|d7!AVUl hjp\<~D4/B wJ_gZAĞu6+nC& KF#ڿv-I 4Iւ_bQJʌؘKnˆͷ2|6Ɠs#S3*N9*lʘ${;`ʐaIfN3]gԋҏNIzz3jTBXkҦ{8hfv @@ -5439,7 +5439,7 @@ W r]s^dONrDP%;X}SP}ʦVRղRl50}\/V泖kaAyRţ2/]K /DAaZD9%=3|4i`u/)%OzMqBVn|ϗj5Ю}ӌ֤sÌT_CٔKX.n\{ν /'J2+tI]) ៤R:j jU7_t NU}>Mkhg,%ո{p] Ъ;QS6$vSْDm{z`9*4f˼ -7r%^LrDLʜ4 ].Ec؁סּ>G 0QS+PyH!_8y:D>[P9]+J=kְ,sR+fS&t +Y]C=3&fzd"Zn|mu_DS*1~_Xh5zJJs] j [?rk@=2+²$f7bDcұI*u&H$Bl7-"33᫁zw_ˋְW4`ۃ*%r%^FH"/JQ;+߷$5B^G”-і,1_q%Ne_,h0hg1Mƻpɓ؉I|G 0QS+PyH!_8y:D>[P9]+J=kְ,sR+fS&t +Y]C=3&fzd"Zn|mu_DS*1~_Xh5zJJs] j [?rk@=2+²$f7bDcұI*u&H$Bl7-"33᫁zw_ˋְW4`ۃ*%r%^FH"/JQ;+߷$5B^G”-і,1_q%Ne_,h0hg1Mƻpɓ؉I|ՔN%GBjէߜ S?9a kKW9CnS' GsNt;5~t|D|y1> endobj 345 0 obj [/ICCBased 318 0 R] endobj 302 0 obj <>stream Hl͎7 :&+(M0 ]y|TOƃx0@Y,CNoy˩O^o~|/9ki{הɵuεZKç{:͜4ҒUZe?==,>+`F`hEV&3(xIJ]4ybMYWvɅWcDfK{RI2`IM%Zw*Ѫd=o@U, -(܋w#RRIPeoF)_h.GփWNGoN}g[m diff --git a/docs/_templates/artwork.html b/docs/_templates/artwork.html index 16db93171..c0f964043 100644 --- a/docs/_templates/artwork.html +++ b/docs/_templates/artwork.html @@ -1,4 +1,3 @@

    Artwork by Kalamun © 2013

    - diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t index 814dea777..bc8563b69 100644 --- a/docs/_themes/flask/static/flasky.css_t +++ b/docs/_themes/flask/static/flasky.css_t @@ -8,11 +8,11 @@ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} - + @import url("basic.css"); - + /* -- page layout ----------------------------------------------------------- */ - + body { font-family: 'Georgia', serif; font-size: 17px; @@ -43,7 +43,7 @@ div.sphinxsidebar { hr { border: 1px solid #B1B4B6; } - + div.body { background-color: #ffffff; color: #3E4349; @@ -54,7 +54,7 @@ img.floatingflask { padding: 0 0 10px 10px; float: right; } - + div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; @@ -70,7 +70,7 @@ div.footer a { div.related { display: none; } - + div.sphinxsidebar a { color: #444; text-decoration: none; @@ -80,7 +80,7 @@ div.sphinxsidebar a { div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } - + div.sphinxsidebar { font-size: 14px; line-height: 1.5; @@ -95,7 +95,7 @@ div.sphinxsidebarwrapper p.logo { margin: 0; text-align: center; } - + div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; @@ -109,7 +109,7 @@ div.sphinxsidebar h4 { div.sphinxsidebar h4 { font-size: 20px; } - + div.sphinxsidebar h3 a { color: #444; } @@ -120,7 +120,7 @@ div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } - + div.sphinxsidebar p { color: #555; margin: 10px 0; @@ -131,25 +131,25 @@ div.sphinxsidebar ul { padding: 0; color: #000; } - + div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } - + /* -- body styles ----------------------------------------------------------- */ - + a { color: #004B6B; text-decoration: underline; } - + a:hover { color: #6D4100; text-decoration: underline; } - + div.body h1, div.body h2, div.body h3, @@ -175,18 +175,18 @@ div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } - + a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } - + a.headerlink:hover { color: #444; background: #eaeaea; } - + div.body p, div.body dd, div.body li { line-height: 1.4em; } @@ -233,20 +233,20 @@ div.note { background-color: #eee; border: 1px solid #ccc; } - + div.seealso { background-color: #ffc; border: 1px solid #ff6; } - + div.topic { background-color: #eee; } - + p.admonition-title { display: inline; } - + p.admonition-title:after { content: ":"; } @@ -348,7 +348,7 @@ ul, ol { margin: 10px 0 10px 30px; padding: 0; } - + pre { background: #eee; padding: 7px 30px; @@ -365,7 +365,7 @@ dl dl pre { margin-left: -90px; padding-left: 90px; } - + tt { background-color: #ecf0f3; color: #222; diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t index fe2141c56..71961a272 100644 --- a/docs/_themes/flask_small/static/flasky.css_t +++ b/docs/_themes/flask_small/static/flasky.css_t @@ -8,11 +8,11 @@ * :license: BSD, see LICENSE for details. * */ - + @import url("basic.css"); - + /* -- page layout ----------------------------------------------------------- */ - + body { font-family: 'Georgia', serif; font-size: 17px; @@ -35,7 +35,7 @@ div.bodywrapper { hr { border: 1px solid #B1B4B6; } - + div.body { background-color: #ffffff; color: #3E4349; @@ -46,7 +46,7 @@ img.floatingflask { padding: 0 0 10px 10px; float: right; } - + div.footer { text-align: right; color: #888; @@ -55,12 +55,12 @@ div.footer { width: 650px; margin: 0 auto 40px auto; } - + div.footer a { color: #888; text-decoration: underline; } - + div.related { line-height: 32px; color: #888; @@ -69,18 +69,18 @@ div.related { div.related ul { padding: 0 0 0 10px; } - + div.related a { color: #444; } - + /* -- body styles ----------------------------------------------------------- */ - + a { color: #004B6B; text-decoration: underline; } - + a:hover { color: #6D4100; text-decoration: underline; @@ -89,7 +89,7 @@ a:hover { div.body { padding-bottom: 40px; /* saved for footer */ } - + div.body h1, div.body h2, div.body h3, @@ -109,24 +109,24 @@ div.indexwrapper h1 { height: {{ theme_index_logo_height }}; } {% endif %} - + div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } - + a.headerlink { color: white; padding: 0 4px; text-decoration: none; } - + a.headerlink:hover { color: #444; background: #eaeaea; } - + div.body p, div.body dd, div.body li { line-height: 1.4em; } @@ -164,25 +164,25 @@ div.note { background-color: #eee; border: 1px solid #ccc; } - + div.seealso { background-color: #ffc; border: 1px solid #ff6; } - + div.topic { background-color: #eee; } - + div.warning { background-color: #ffe4e4; border: 1px solid #f66; } - + p.admonition-title { display: inline; } - + p.admonition-title:after { content: ":"; } @@ -254,7 +254,7 @@ dl { dl dd { margin-left: 30px; } - + pre { padding: 0; margin: 15px -30px; diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 33f47449c..0dcf53b75 100644 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,7 +1,19 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal +from pygments.token import ( + Keyword, + Name, + Comment, + String, + Error, + Number, + Operator, + Generic, + Whitespace, + Punctuation, + Other, + Literal, +) class FlaskyStyle(Style): @@ -10,77 +22,68 @@ class FlaskyStyle(Style): styles = { # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - + # Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + Number: "#990000", # class: 'm' + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' } diff --git a/docs/authentication.rst b/docs/authentication.rst index 1ee357672..cd831a25d 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -23,7 +23,7 @@ why you are provided with a handful of base authentication classes. They implement the basic authentication mechanism and must be subclassed in order to implement authorization logic. No matter which authentication scheme you pick the only thing that you need to do in your subclass is override the -``check_auth()`` method. +``check_auth()`` method. Global Authentication --------------------- @@ -56,7 +56,7 @@ to provide the correct credentials in order to consume the API: HTTP/1.1 200 OK By default access is restricted to all endpoints for all HTTP verbs -(methods), effectively locking down the whole API. +(methods), effectively locking down the whole API. But what if your authorization logic is more complex, and you only want to secure some endpoints or apply different logics depending on the @@ -76,14 +76,14 @@ authentication class, maybe with something like this: If needed, this approach also allows to take the request ``method`` into consideration, for example to allow ``GET`` requests for everyone while forcing -validation on edits (``POST``, ``PUT``, ``PATCH``, ``DELETE``). +validation on edits (``POST``, ``PUT``, ``PATCH``, ``DELETE``). Endpoint-level Authentication ----------------------------- The *one class to bind them all* approach seen above is probably good for most use cases but as soon as authorization logic gets more complicated it could easily lead to complex and unmanageable code, something you don't really want -to have when dealing with security. +to have when dealing with security. Wouldn't it be nice if we could have specialized auth classes that we could freely apply to selected endpoints? This way the global level auth class, the @@ -93,7 +93,7 @@ Alternatively, we could even choose to *not* provide a global auth class, effectively making all endpoints public, except the ones we want protected. With a system like this we could even choose to have some endpoints protected with, say, Basic Authentication while others are secured with Token, or HMAC -Authentication! +Authentication! Well, turns out this is actually possible by simply enabling the resource-level ``authentication`` setting when we are defining the API @@ -106,7 +106,7 @@ resource-level ``authentication`` setting when we are defining the API 'authentication': MySuperCoolAuth, ... }, - 'invoices': ... + 'invoices': ... } And that's it. The `people` endpoint will now be using the ``MySuperCoolAuth`` @@ -118,7 +118,7 @@ There are other features and options that you can use to reduce complexity in your auth classes, especially (but not only) when using the global level authentication system. Lets review them. -Global Endpoint Security +Global Endpoint Security ------------------------ You might want a public read-only API where only authorized users can write, edit and delete. You can achieve that by using the ``PUBLIC_METHODS`` and @@ -127,7 +127,7 @@ your `settings.py`: :: - PUBLIC_METHODS = ['GET'] + PUBLIC_METHODS = ['GET'] PUBLIC_ITEM_METHODS = ['GET'] And run your API. POST, PATCH and DELETE are still restricted, while GET is @@ -159,7 +159,7 @@ first open read access for all endpoints: :: - PUBLIC_METHODS = ['GET'] + PUBLIC_METHODS = ['GET'] PUBLIC_ITEM_METHODS = ['GET'] Then you protect the private endpoint: @@ -188,7 +188,7 @@ Basic Authentication with bcrypt Encoding passwords with bcrypt_ is a great idea. It comes at the cost of performance, but that's precisely the point, as slow encoding means very good resistance to brute-force attacks. For a faster (and less safe) alternative, see -the SHA1/MAC snippet further below. +the SHA1/MAC snippet further below. This script assumes that user accounts are stored in an `accounts` MongoDB collection, and that passwords are stored as bcrypt hashes. All API @@ -327,7 +327,7 @@ HMAC Authentication The ``eve.auth.HMACAuth`` class allows for custom, Amazon S3-like, HMAC (Hash Message Authentication Code) authentication, which is basically a very secure custom authentication scheme built around the `Authorization` header. - + How HMAC Authentication Works ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The server provides the client with a user id and a secret key through some @@ -337,7 +337,7 @@ secret key to sign all requests. When the client wants to send a request, he builds the complete request and then, using the secret key, computes a hash over the complete message body (and -optionally some of the message headers if required) +optionally some of the message headers if required) Next, the client adds the computed hash and his userid to the message in the Authorization header: @@ -360,7 +360,7 @@ temporarily work on your behalf. This is also the reason why the secret key is generally provided through out-of-band channels (often a webpage or, as said above, an email or plain old paper). -The ``eve.auth.HMACAuth`` class also support access roles. +The ``eve.auth.HMACAuth`` class also support access roles. HMAC Example ~~~~~~~~~~~~ @@ -379,7 +379,7 @@ Eve `repository`_. class HMACAuth(HMACAuth): def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, resource, method): - # use Eve's own db driver; no additional connections/resources are + # use Eve's own db driver; no additional connections/resources are # used accounts = app.data.driver.db['accounts'] user = accounts.find_one({'userid': userid}) @@ -413,7 +413,7 @@ settings ` (or the corresponding ``allowed_roles`` and ``allowed_item_ro ALLOWED_ROLES = ['admin'] Then your subclass would implement the authorization logic by making good use -of the aforementioned ``allowed_roles`` parameter. +of the aforementioned ``allowed_roles`` parameter. The snippet below assumes that user accounts are stored in an `accounts` MongoDB collection, that passwords are stored as SHA1/HMAC hashes and that user @@ -458,7 +458,7 @@ unless they are made explicitly public. if __name__ == '__main__': app = Eve(auth=RolesAuth) app.run() - + .. _user-restricted: User-Restricted Resource Access @@ -508,7 +508,7 @@ BCrypt-authentication example from above: # use Eve's own db driver; no additional connections/resources are used accounts = app.data.driver.db['accounts'] account = accounts.find_one({'username': username}) - # set 'auth_field' value to the account's ObjectId + # set 'auth_field' value to the account's ObjectId # (instead of _id, you might want to use ID_FIELD) if account and '_id' in account: self.set_request_auth_value(account['_id']) @@ -525,7 +525,7 @@ BCrypt-authentication example from above: Auth-driven Database Access --------------------------- Custom authentication classes can also set the database that should be used -when serving the active request. +when serving the active request. Normally you either use a single database for the whole API or you configure which database each endpoint consumes by setting ``mongo_prefix`` to the @@ -559,7 +559,7 @@ A trivial example would be: The above class will serve ``user1`` with data coming from the database which configuration settings are prefixed by ``MONGO1`` in ``settings.py``. Same happens with ``user2`` and ``MONGO2`` while all other users are served with -the default database. +the default database. Since values set by ``set_mongo_prefix()`` have precedence over both default and endpoint-level ``mongo_prefix`` settings, what happens here is that the two diff --git a/docs/changelog.rst b/docs/changelog.rst index 35d8df15c..f6a352994 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,3 @@ .. _changelog: .. include:: ../CHANGES.rst - - diff --git a/docs/extensions.rst b/docs/extensions.rst index 01ee8b133..5abcd2429 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -4,7 +4,7 @@ Extensions Welcome to the Eve extensions registry. Here you can find a list of packages that extend Eve. This list is moderated and updated on a regular basis. If you wrote a package for Eve and want it to show up here, just `get in touch`_ and -show me your tool! +show me your tool! - Eve-Auth-JWT_ - Eve-Elastic_ @@ -60,14 +60,14 @@ Eve-Mongoengine_ is an Eve extension, which enables Mongoengine ORM models to be used as eve schema. If you use mongoengine in your application and simultaneously want to use Eve, instead of writing schema again in Cerberus format (DRY!), you can use this extension, which takes your mongoengine models -and auto-transforms them into Cerberus schema under the hood. +and auto-transforms them into Cerberus schema under the hood. Eve-Neo4j --------- *by Abraxas Biosystems* -Eve-Neo4j_ is an Eve extension aiming to enable it's users to build and -deploy highly customizable, fully featured RESTful Web Services using Neo4j +Eve-Neo4j_ is an Eve extension aiming to enable it's users to build and +deploy highly customizable, fully featured RESTful Web Services using Neo4j as backend. Powered by Eve, Py2neo, flask-neo4j and good intentions. Eve-OAuth2 @@ -75,7 +75,7 @@ Eve-OAuth2 *by Nicola Iarocci* Eve-OAuth2_ is not an extension per-se, but rather an example of how you can -leverage Flask-Sentinel_ to protect your API endpoints with OAuth2. +leverage Flask-Sentinel_ to protect your API endpoints with OAuth2. Eve-SQLAlchemy -------------- diff --git a/docs/features.rst b/docs/features.rst index 0e61c6543..c2d3ac310 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -322,7 +322,7 @@ Native Python syntax works like this: HTTP/1.1 200 OK Both syntaxes allow for conditional and logical And/Or operators, however -nested and combined. +nested and combined. Filters are enabled by default on all document fields. However, the API maintainer can choose to disable them all and/or whitelist allowed ones (see @@ -367,7 +367,7 @@ You can pretty print the response by specifying a query parameter named "_created": "Tue, 19 Apr 2016 08:19:00 GMT", "_id": "5715e9f438345b3510d27eb8", "_etag": "86dc6b45fe7e2f41f1ca53a0e8fda81224229799" - }, + }, ... ] } @@ -638,7 +638,7 @@ metadata: When a ``201 Created`` is returned following a POST request, the ``Location`` header is also included with the response. Its value is the URI to the new -document. +document. In order to reduce the number of loopbacks, a client might also submit multiple documents with a single request. All it needs to do is enclose the @@ -676,7 +676,7 @@ The response will be a list itself, with the state of each document: When multiple documents are submitted the API takes advantage of MongoDB *bulk insert* capabilities which means that not only there's just one request traveling from the client to the remote API, but also that a single loopback is -performed between the API server and the database. +performed between the API server and the database. In case of successful multiple inserts, keep in mind that the ``Location`` header only returns the URI of the first created document. @@ -1849,7 +1849,7 @@ encoded in GeoJSON_ format. All GeoJSON objects supported by MongoDB_ are availa - ``GeometryCollection`` All these objects are implemented as native Eve data types (see :ref:`schema`) -so they are are subject to the proper validation. +so they are are subject to the proper validation. In the example below we are extending the `people` endpoint by adding a ``location`` field is of type Point_. diff --git a/docs/foreword.rst b/docs/foreword.rst index 35cb75a7b..917da2e39 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -10,7 +10,7 @@ should not be using it. Philosophy ---------- You have data stored somewhere and you want to expose it to your users -through a RESTful Web API. Eve is the tool that allows you to do so. +through a RESTful Web API. Eve is the tool that allows you to do so. Eve provides a robust, feature rich, REST-centered API implementation, and you just need to configure your API settings and behavior, plug in your @@ -48,7 +48,7 @@ MongoDB*, are `available online`_. You might want to check them out to understan why and how certain design decisions were made, especially with regards to REST implementation. -BSD License +BSD License ----------- A large number of open source projects you find today are GPL Licensed. While the GPL has its time and place, it should most certainly not be your go-to diff --git a/docs/funding.rst b/docs/funding.rst index 3f0ed5e78..7352bcc0b 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -18,7 +18,7 @@ If you run a business and is using Eve in a revenue-generating product, it would make business sense to sponsor Eve development: it ensures the project that your product relies on stays healthy and actively maintained. It can also help your exposure in the Eve community and makes it easier to attract Eve -developers. +developers. Of course, individual users are also welcome to make a recurring pledge if Eve has helped you in your work or personal projects. Alternatively, consider diff --git a/docs/index.rst b/docs/index.rst index 58eb0687e..9b67e0545 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,6 +20,9 @@ Version |version|. .. image:: https://img.shields.io/badge/license-BSD-blue.svg?style=flat-square :target: https://en.wikipedia.org/wiki/BSD_License +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + ----- Eve is an :doc:`open source ` Python REST API framework designed for @@ -28,7 +31,7 @@ fully featured RESTful Web Services. Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is provided by -community extensions_. +community extensions_. The codebase is thoroughly tested under Python 2.7, 3.4+, and PyPy. @@ -60,7 +63,7 @@ business sense to sponsor Eve development: it ensures the project that your product relies on stays healthy and actively maintained. Individual users are also welcome to make either a recurring pledge or a one time donation if Eve has helped you in your work or personal projects. Every single sign-up makes -a significant impact towards making Eve possible. +a significant impact towards making Eve possible. To join the backer ranks, check out `Eve campaign on Patreon`_. @@ -106,7 +109,7 @@ link `_. .. note:: This documentation is under constant development. Please refer to the links - on the sidebar for more information. + on the sidebar for more information. .. _python-eve.org: http://python-eve.org diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 11246482c..191c24872 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,13 +3,13 @@ Quickstart ========== -Eager to get started? This page gives a first introduction to Eve. +Eager to get started? This page gives a first introduction to Eve. Prerequisites ------------- - You already have Eve installed. If you do not, head over to the :ref:`install` section. -- MongoDB is installed_. +- MongoDB is installed_. - An instance of MongoDB is running_. A Minimal Application @@ -34,7 +34,7 @@ Save it as settings.py in the same directory where run.py is stored. This is the Eve configuration file, a standard Python module, and it is telling Eve that your API is comprised of just one accessible resource, ``people``. -Now your are ready to launch your API. +Now your are ready to launch your API. .. code-block:: console @@ -61,7 +61,7 @@ payload: "_links": { "child": [ { - "href": "people", + "href": "people", "title": "people" } ] @@ -81,14 +81,14 @@ Try requesting ``people`` now: :: { - "_items": [], + "_items": [], "_links": { "self": { - "href": "people", + "href": "people", "title": "people" - }, + }, "parent": { - "href": "/", + "href": "/", "title": "home" } } @@ -100,7 +100,7 @@ page) and to the resource itself. If you got a timeout error from pymongo, make sure the prerequistes are met. Chances are that the ``mongod`` server process is not runnig. -By default Eve APIs are read-only: +By default Eve APIs are read-only: .. code-block:: console @@ -204,10 +204,10 @@ Let's define a schema for our ``people`` resource. }, } -For more information on validation see :ref:`validation`. +For more information on validation see :ref:`validation`. Now let's say that we want to further customize the ``people`` endpoint. We want -to: +to: - set the item title to ``person`` - add an extra :ref:`custom item endpoint ` at ``/people/`` @@ -226,7 +226,7 @@ file: # by default the standard item entry point is defined as # '/people/'. We leave it untouched, and we also enable an - # additional read-only entry point. This way consumers can also perform + # additional read-only entry point. This way consumers can also perform # GET requests at '/people/'. 'additional_lookup': { 'url': 'regex("[\w]+")', @@ -268,10 +268,10 @@ endpoint: $ curl -i http://127.0.0.1:5000/people/obama HTTP/1.0 200 OK Etag: 28995829ee85d69c4c18d597a0f68ae606a266cc - Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT + Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT Cache-Control: 'max-age=10,must-revalidate' Expires: 10 - ... + ... .. code-block:: javascript diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index 6f4c65a5f..7388dc6fa 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -16,10 +16,10 @@ Eve REST API for Humans™ has been presented at the following events so far: - PiterPy 2016, St. Petersburg - Percona Live 2015, Amsterdam - EuroPython 2014, Berlin -- Python Meetup, Helsinki -- PyCon Italy 2014, Florence -- PyCon Sweden 2014, Stockholm -- FOSDEM 2014, Brussels +- Python Meetup, Helsinki +- PyCon Italy 2014, Florence +- PyCon Sweden 2014, Stockholm +- FOSDEM 2014, Brussels Want this talk delivered at your conference? Get in touch_! diff --git a/docs/snippets/hooks_blueprints.rst b/docs/snippets/hooks_blueprints.rst index bdc65418e..80366fccc 100644 --- a/docs/snippets/hooks_blueprints.rst +++ b/docs/snippets/hooks_blueprints.rst @@ -47,7 +47,7 @@ properly MongoDB collection. {"$set": {"user": None}}, multi=True ) - + app = Eve() # register the blueprint to the main Eve application app.register_blueprint(blueprint) diff --git a/docs/snippets/template.rst b/docs/snippets/template.rst index 16e268d1e..3d7338a71 100644 --- a/docs/snippets/template.rst +++ b/docs/snippets/template.rst @@ -14,4 +14,4 @@ experience. Make your code snippet follow, like so: app = Eve() app.run() -Add closing comments as needed. +Add closing comments as needed. diff --git a/docs/support.rst b/docs/support.rst index ddc430740..ec5644fd3 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -9,13 +9,13 @@ are several options: Stack Overflow -------------- `Stack Overflow`_ has a eve tag. It is generally followed by Eve developers -and users. +and users. Mailing List ------------ The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting -feedback. +feedback. IRC --- diff --git a/docs/tutorials/account_management.rst b/docs/tutorials/account_management.rst index e00e88307..3ebf7dc38 100644 --- a/docs/tutorials/account_management.rst +++ b/docs/tutorials/account_management.rst @@ -17,11 +17,11 @@ consumed by the accounts themselves? In the following paragraphs we'll see a couple of possible Account Management implementations, both making intensive use of a host of Eve features such as :ref:`endpointsec`, :ref:`roleaccess`, :ref:`user-restricted`, -:ref:`eventhooks`. +:ref:`eventhooks`. We assume that SSL/TLS is enabled, which means that our transport layer is encrypted, making both :ref:`basic` and :ref:`token` valid options to secure API -endpoints. +endpoints. Let's say we're upgrading the API we defined in the :ref:`quickstart` tutorial. @@ -32,7 +32,7 @@ Accounts with Basic Authentication Our tasks are as follows: 1. Make an endpoint available for all account management activities - (``/accounts``). + (``/accounts``). 2. Secure the endpoint, so that it is only accessible to clients that we control: our own website, mobile apps with account management capabilities, etc. @@ -66,8 +66,8 @@ Then, let's define the endpoint. accounts = { # the standard account entry point is defined as - # '/accounts/'. We define an additional read-only entry - # point accessible at '/accounts/'. + # '/accounts/'. We define an additional read-only entry + # point accessible at '/accounts/'. 'additional_lookup': { 'url': 'regex("[\w]+")', 'field': 'username', @@ -136,7 +136,7 @@ with simple POST requests, of course authenticating itself as a `superuser` by means of the `Authorization` header. The script assumes that stored passwords are encrypted with `bcrypt` (storing passwords as plain text is *never* a good idea). See :ref:`basic` for an alternative, faster but less secure SHA1/MAC -example. +example. 2b. User Roles Access Control ''''''''''''''''''''''''''''' @@ -170,7 +170,7 @@ Let's start by updating our resource schema. }, We just added a new ``roles`` field which is a required list. From now on, one -or more roles will have to be assigned on account creation. +or more roles will have to be assigned on account creation. Now we need to restrict endpoint access to `superuser` and `admin` accounts only so let's update the endpoint definition accordingly. @@ -180,8 +180,8 @@ only so let's update the endpoint definition accordingly. accounts = { # the standard account entry point is defined as - # '/accounts/'. We define an additional read-only entry - # point accessible at '/accounts/'. + # '/accounts/'. We define an additional read-only entry + # point accessible at '/accounts/'. 'additional_lookup': { 'url': 'regex("[\w]+")', 'field': 'username', @@ -194,7 +194,7 @@ only so let's update the endpoint definition accordingly. # Only allow superusers and admins. 'allowed_roles': ['superuser', 'admin'], - + # Finally, let's add the schema definition for this endpoint. 'schema': schema, } @@ -274,7 +274,7 @@ value: .. code-block:: python :emphasize-lines: 15-17 - + from eve import Eve from eve.auth import BasicAuth @@ -290,7 +290,7 @@ value: # only retrieve a user if his roles match ``allowed_roles`` lookup['roles'] = {'$in': allowed_roles} account = accounts.find_one(lookup) - # set 'AUTH_FIELD' value to the account's ObjectId + # set 'AUTH_FIELD' value to the account's ObjectId # (instead of _Id, you might want to use ID_FIELD) self.set_request_auth_value(account['_id']) return account and check_password_hash(account['password'], password) @@ -316,12 +316,12 @@ the token, and the password field is not provided (if included, it is ignored). Consequently, handling accounts with Token Authentication is very similar to what we saw in :ref:`accounts_basic`, but there's one little caveat: tokens need to be generated and stored along with the account, and eventually returned -to the client. +to the client. In light of this, let's review our updated task list: - + 1. Make an endpoint available for all account management activities - (``/accounts``). + (``/accounts``). 2. Secure the endpoint so that it is only accessible to clients (tokens) that we control. 3. On account creation, generate and store its token. @@ -362,7 +362,7 @@ need to add the `token` field to our schema: 2. Securing the ``/accounts/`` endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We defined the `roles` field for the `accounts` schema in the previous step. -We also need to define the endpoint, making sure that we set the allowed +We also need to define the endpoint, making sure that we set the allowed user roles. .. code-block:: python @@ -370,8 +370,8 @@ user roles. accounts = { # the standard account entry point is defined as - # '/accounts/'. We define an additional read-only entry - # point accessible at '/accounts/'. + # '/accounts/'. We define an additional read-only entry + # point accessible at '/accounts/'. 'additional_lookup': { 'url': 'regex("[\w]+")', 'field': 'username', @@ -384,7 +384,7 @@ user roles. # Only allow superusers and admins. 'allowed_roles': ['superuser', 'admin'], - + # Finally, let's add the schema definition for this endpoint. 'schema': schema, } @@ -448,7 +448,7 @@ to be stored to the database. # Don't use this in production: # You should at least make sure that the token is unique. for document in documents: - document["token"] = (''.join(random.choice(string.ascii_uppercase) + document["token"] = (''.join(random.choice(string.ascii_uppercase) for x in range(10))) @@ -485,8 +485,8 @@ definition accordingly: accounts = { # the standard account entry point is defined as - # '/accounts/'. We define an additional read-only entry - # point accessible at '/accounts/'. + # '/accounts/'. We define an additional read-only entry + # point accessible at '/accounts/'. 'additional_lookup': { 'url': 'regex("[\w]+")', 'field': 'username', @@ -502,7 +502,7 @@ definition accordingly: # Allow 'token' to be returned with POST responses 'extra_response_fields': ['token'], - + # Finally, let's add the schema definition for this endpoint. 'schema': schema, } @@ -531,7 +531,7 @@ Despite being a little more tricky to set up on the server side, Token Authentication offers significant advantages. First, you don't have passwords stored on the client and being sent over the wire with every request. If you're sending your tokens out-of-band, and you're on SSL/TLS, that's quite -a lot of additional security. +a lot of additional security. .. _SSL/TLS: http://en.wikipedia.org/wiki/Transport_Layer_Security .. _`Event Hooks`: http://python-eve.org/features.html#event-hooks diff --git a/docs/tutorials/custom_idfields.rst b/docs/tutorials/custom_idfields.rst index af5ef5afb..912a5c6ad 100644 --- a/docs/tutorials/custom_idfields.rst +++ b/docs/tutorials/custom_idfields.rst @@ -9,11 +9,11 @@ you configure a ``/invoices`` endpoint, which will allow clients to query the underlying `invoices` database collection. The ``/invoices/`` endpoint will be made available by the framework, and will be used by clients to retrieve and/or edit individual documents. By default, Eve provides this feature -seamlessly when ``ID_FIELD`` fields are of ``ObjectId`` type. +seamlessly when ``ID_FIELD`` fields are of ``ObjectId`` type. However, you might have collections where your unique identifier is not and ``ObjectId``, and you still want individual document endpoints to work -properly. Don't worry, it's doable, it only requires a little tinkering. +properly. Don't worry, it's doable, it only requires a little tinkering. Handling ``UUID`` fields ------------------------ @@ -63,7 +63,7 @@ serialization magic: # will properly render ObjectIds, datetimes, etc.) return super(UUIDEncoder, self).default(obj) - + ``UUID`` Validation ~~~~~~~~~~~~~~~~~~~ By default Eve creates a unique identifier for each newly inserted document, @@ -91,7 +91,7 @@ details on custom validation): ``UUID`` URLs ~~~~~~~~~~~~~ Now Eve is capable of rendering and validating UUID values but it still doesn't know -which resources are going to use these features. We also need to set +which resources are going to use these features. We also need to set ``item_url`` so uuid formed urls can be properly parsed. Let's pick our ``settings.py`` module and update the API domain accordingly: @@ -117,7 +117,7 @@ regex in order to avoid setting it for every single resource endpoint. Passing the ``UUID`` juice to Eve ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now all the missing pieces are there we only need to instruct Eve on how to -use them. Eve needs to know about the new data type when its building the +use them. Eve needs to know about the new data type when its building the URL map, so we need to pass our custom classes right at the beginning, when we are instancing the application: diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 5d14a0000..ee85e8e84 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -8,7 +8,7 @@ Tutorials account_management custom_idfields - + Learn Eve at TalkPython Training -------------------------------- There is a 5 hours-long Eve course available for you at the fine TalkPython diff --git a/docs/updates.rst b/docs/updates.rst index 1de0a37da..e7b3a2a69 100644 --- a/docs/updates.rst +++ b/docs/updates.rst @@ -7,7 +7,7 @@ there are several options: Blog ---- -`Eve News `_ is the official blog of the Eve project. +`Eve News `_ is the official blog of the Eve project. Twitter ------- @@ -18,7 +18,7 @@ Mailing List ------------ The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting -feedback. +feedback. GitHub ------ diff --git a/docs/validation.rst b/docs/validation.rst index c0dd9968d..c6f2f7722 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -32,7 +32,7 @@ request: ] In the example above, the first document did not validate so the whole request -has been rejected. +has been rejected. When all documents pass validation and are inserted correctly the response status is ``201 Created``. If any document fails validation the response status @@ -40,7 +40,7 @@ is ``422 Unprocessable Entity``, or any other error code defined by ``VALIDATION_ERROR_STATUS`` configuration. For information on how to define documents schema and standard validation -rules, see :ref:`schema`. +rules, see :ref:`schema`. Extending Data Validation ------------------------- @@ -82,7 +82,7 @@ can now do something like: 'schema': { 'oddity': { - 'isodd': True, + 'isodd': True, 'type': 'integer' } } @@ -120,7 +120,7 @@ allowing something like this: You can also check the `source code`_ for Eve custom validation, where you will find more advanced use cases, such as the implementation of the ``unique`` and -``data_relation`` constraints. +``data_relation`` constraints. For more information on @@ -128,8 +128,8 @@ For more information on We have only scratched the surface of data validation. Please make sure to check the Cerberus_ documentation for a complete list of available - validation rules and data types. - + validation rules and data types. + Also note that Cerberus requirement is pinned to version 0.9.2, which still supports the ``validate_update`` method used for ``PATCH`` requests. Upgrade to Cerberus 1.0+ is scheduled for Eve version 0.8. @@ -178,7 +178,7 @@ a payload like this will be accepted: option is enabled, clients will be capable of actually `adding` fields via PATCH (edit). -``ALLOW_UNKNOWN`` is also useful for read-only APIs or endpoints that +``ALLOW_UNKNOWN`` is also useful for read-only APIs or endpoints that need to return the whole document, as found in the underlying database. In this scenario you don't want to bother with validation schemas. For the whole API just set ``ALLOW_UNKNOWN`` to ``True``, then ``schema: {}`` at every endpoint. diff --git a/eve/__init__.py b/eve/__init__.py index e3ce7809b..385ed5b54 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,49 +38,49 @@ """ -__version__ = '0.8.1.dev0' +__version__ = "0.8.1.dev0" # RFC 1123 (ex RFC 822) -DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' -RFC1123_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' +DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" +RFC1123_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -URL_PREFIX = '' -API_VERSION = '' +URL_PREFIX = "" +API_VERSION = "" PAGINATION = True PAGINATION_LIMIT = 50 PAGINATION_DEFAULT = 25 -ID_FIELD = '_id' -CACHE_CONTROL = 'max-age=10,must-revalidate' # TODO confirm this value +ID_FIELD = "_id" +CACHE_CONTROL = "max-age=10,must-revalidate" # TODO confirm this value CACHE_EXPIRES = 10 ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False -RESOURCE_METHODS = ['GET'] -ITEM_METHODS = ['GET'] +RESOURCE_METHODS = ["GET"] +ITEM_METHODS = ["GET"] ITEM_LOOKUP = True ITEM_LOOKUP_FIELD = ID_FIELD ITEM_URL = 'regex("[a-f0-9]{24}")' STATUS_OK = "OK" STATUS_ERR = "ERR" -LAST_UPDATED = '_updated' -DATE_CREATED = '_created' -ISSUES = '_issues' -STATUS = '_status' -ERROR = '_error' -ITEMS = '_items' -LINKS = '_links' -ETAG = '_etag' -VERSION = '_version' -META = '_meta' +LAST_UPDATED = "_updated" +DATE_CREATED = "_created" +ISSUES = "_issues" +STATUS = "_status" +ERROR = "_error" +ITEMS = "_items" +LINKS = "_links" +ETAG = "_etag" +VERSION = "_version" +META = "_meta" INFO = None -QUERY_WHERE = 'where' -QUERY_SORT = 'sort' -QUERY_PAGE = 'page' -QUERY_MAX_RESULTS = 'max_results' -QUERY_EMBEDDED = 'embedded' -QUERY_PROJECTION = 'projection' +QUERY_WHERE = "where" +QUERY_SORT = "sort" +QUERY_PAGE = "page" +QUERY_MAX_RESULTS = "max_results" +QUERY_EMBEDDED = "embedded" +QUERY_PROJECTION = "projection" VALIDATION_ERROR_STATUS = 422 VALIDATION_ERROR_AS_LIST = False diff --git a/eve/auth.py b/eve/auth.py index 6cc063188..122d9af6f 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -30,53 +30,57 @@ def requires_auth(endpoint_class): .. versionadded:: 0.0.4 """ + def fdec(f): @wraps(f) def decorated(*args, **kwargs): - if endpoint_class == 'resource' or endpoint_class == 'item': + if endpoint_class == "resource" or endpoint_class == "item": if args: resource_name = args[0] - elif kwargs.get('resource'): - resource_name = kwargs.get('resource') + elif kwargs.get("resource"): + resource_name = kwargs.get("resource") else: - raise ValueError("'requires_auth(%s)' decorated functions " - "must include resource in args or kwargs" - % endpoint_class) + raise ValueError( + "'requires_auth(%s)' decorated functions " + "must include resource in args or kwargs" % endpoint_class + ) # fetch resource or item auth configuration - resource = app.config['DOMAIN'].get(resource_name) + resource = app.config["DOMAIN"].get(resource_name) if resource is None: abort(404) - if endpoint_class == 'resource': - public = resource['public_methods'] - roles = list(resource['allowed_roles']) - if request.method in ['GET', 'HEAD', 'OPTIONS']: - roles += resource['allowed_read_roles'] + if endpoint_class == "resource": + public = resource["public_methods"] + roles = list(resource["allowed_roles"]) + if request.method in ["GET", "HEAD", "OPTIONS"]: + roles += resource["allowed_read_roles"] else: - roles += resource['allowed_write_roles'] - elif endpoint_class == 'item': - public = resource['public_item_methods'] - roles = list(resource['allowed_item_roles']) - if request.method in ['GET', 'HEAD', 'OPTIONS']: - roles += resource['allowed_item_read_roles'] + roles += resource["allowed_write_roles"] + elif endpoint_class == "item": + public = resource["public_item_methods"] + roles = list(resource["allowed_item_roles"]) + if request.method in ["GET", "HEAD", "OPTIONS"]: + roles += resource["allowed_item_read_roles"] else: - roles += resource['allowed_item_write_roles'] + roles += resource["allowed_item_write_roles"] auth = resource_auth(resource_name) else: # home or media endpoints resource_name = resource = None - public = app.config['PUBLIC_METHODS'] + ['OPTIONS'] - roles = list(app.config['ALLOWED_ROLES']) - if request.method in ['GET', 'OPTIONS']: - roles += app.config['ALLOWED_READ_ROLES'] + public = app.config["PUBLIC_METHODS"] + ["OPTIONS"] + roles = list(app.config["ALLOWED_ROLES"]) + if request.method in ["GET", "OPTIONS"]: + roles += app.config["ALLOWED_READ_ROLES"] else: - roles += app.config['ALLOWED_WRITE_ROLES'] + roles += app.config["ALLOWED_WRITE_ROLES"] auth = app.auth if auth and request.method not in public: if not auth.authorized(roles, resource_name, request.method): return auth.authenticate() return f(*args, **kwargs) + return decorated + return fdec @@ -107,20 +111,21 @@ class BasicAuth(object): .. versionadded:: 0.0.4 """ + def set_mongo_prefix(self, value): g.mongo_prefix = value def get_mongo_prefix(self): - return g.get('mongo_prefix') + return g.get("mongo_prefix") def set_request_auth_value(self, value): g.auth_value = value def get_request_auth_value(self): - return g.get('auth_value') + return g.get("auth_value") def get_user_or_token(self): - return g.get('user') + return g.get("user") def set_user_or_token(self, user): g.user = user @@ -141,10 +146,10 @@ def authenticate(self): """ Returns a standard a 401 response that enables basic auth. Override if you want to change the response and/or the realm. """ - resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' % - __package__}) - abort(401, description='Please provide proper credentials', - response=resp) + resp = Response( + None, 401, {"WWW-Authenticate": 'Basic realm="%s"' % __package__} + ) + abort(401, description="Please provide proper credentials", response=resp) def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. @@ -156,8 +161,9 @@ def authorized(self, allowed_roles, resource, method): auth = request.authorization if auth: self.set_user_or_token(auth.username) - return auth and self.check_auth(auth.username, auth.password, - allowed_roles, resource, method) + return auth and self.check_auth( + auth.username, auth.password, allowed_roles, resource, method + ) class HMACAuth(BasicAuth): @@ -179,8 +185,10 @@ class HMACAuth(BasicAuth): .. versionadded:: 0.0.5 """ - def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, - resource, method): + + def check_auth( + self, userid, hmac_hash, headers, data, allowed_roles, resource, method + ): """ This function is called to check if a token is valid. Must be overridden with custom logic. @@ -198,7 +206,7 @@ def authenticate(self): """ Returns a standard a 401. Override if you want to change the response. """ - abort(401, description='Please provide proper credentials') + abort(401, description="Please provide proper credentials") def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. @@ -207,15 +215,21 @@ def authorized(self, allowed_roles, resource, method): string or a list of roles. :param resource: resource being requested. """ - auth = request.headers.get('Authorization') + auth = request.headers.get("Authorization") try: - userid, hmac_hash = auth.split(':') + userid, hmac_hash = auth.split(":") self.set_user_or_token(userid) except: auth = None - return auth and self.check_auth(userid, hmac_hash, request.headers, - request.get_data(), allowed_roles, - resource, method) + return auth and self.check_auth( + userid, + hmac_hash, + request.headers, + request.get_data(), + allowed_roles, + resource, + method, + ) class TokenAuth(BasicAuth): @@ -234,6 +248,7 @@ class TokenAuth(BasicAuth): .. versionadded:: 0.0.5 """ + def check_auth(self, token, allowed_roles, resource, method): """ This function is called to check if a token is valid. Must be overridden with custom logic. @@ -249,10 +264,10 @@ def authenticate(self): """ Returns a standard a 401. Override if you want to change the response. """ - resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' % - __package__}) - abort(401, description='Please provide proper credentials', - response=resp) + resp = Response( + None, 401, {"WWW-Authenticate": 'Basic realm="%s"' % __package__} + ) + abort(401, description="Please provide proper credentials", response=resp) def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. @@ -262,7 +277,7 @@ def authorized(self, allowed_roles, resource, method): :param resource: resource being requested. """ auth = None - if hasattr(request.authorization, 'username'): + if hasattr(request.authorization, "username"): auth = request.authorization.username # Werkzeug parse_authorization does not handle @@ -270,15 +285,14 @@ def authorized(self, allowed_roles, resource, method): # "Authorization: Token " or # "Authorization: Bearer " # headers, therefore they should be explicitly handled - if not auth and request.headers.get('Authorization'): - auth = request.headers.get('Authorization').strip() - if auth.lower().startswith(('token', 'bearer')): - auth = auth.split(' ')[1] + if not auth and request.headers.get("Authorization"): + auth = request.headers.get("Authorization").strip() + if auth.lower().startswith(("token", "bearer")): + auth = auth.split(" ")[1] if auth: self.set_user_or_token(auth) - return auth and self.check_auth(auth, allowed_roles, resource, - method) + return auth and self.check_auth(auth, allowed_roles, resource, method) def auth_field_and_value(resource): @@ -290,21 +304,24 @@ def auth_field_and_value(resource): .. versionadded:: 0.3 """ - if request.endpoint and '|resource' in request.endpoint: + if request.endpoint and "|resource" in request.endpoint: # We are on a resource endpoint and need to check against # `public_methods` - public_method_list_to_check = 'public_methods' + public_method_list_to_check = "public_methods" else: # We are on an item endpoint and need to check against # `public_item_methods` - public_method_list_to_check = 'public_item_methods' + public_method_list_to_check = "public_item_methods" - resource_dict = app.config['DOMAIN'][resource] + resource_dict = app.config["DOMAIN"][resource] auth = resource_auth(resource) request_auth_value = auth.get_request_auth_value() if auth else None - auth_field = resource_dict.get('auth_field', None) if request.method not \ - in resource_dict[public_method_list_to_check] else None + auth_field = ( + resource_dict.get("auth_field", None) + if request.method not in resource_dict[public_method_list_to_check] + else None + ) return auth_field, request_auth_value @@ -318,7 +335,7 @@ def resource_auth(resource): .. versionadded:: 0.5.2 """ - resource_def = app.config['DOMAIN'][resource] - if callable(resource_def['authentication']): - resource_def['authentication'] = resource_def['authentication']() - return resource_def['authentication'] + resource_def = app.config["DOMAIN"][resource] + if callable(resource_def["authentication"]): + resource_def["authentication"] = resource_def["authentication"]() + return resource_def["authentication"] diff --git a/eve/default_settings.py b/eve/default_settings.py index 2ca0472a5..7f318618f 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -103,21 +103,21 @@ # DEBUG = True # RFC 1123 (ex RFC 822) -DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' +DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" STATUS_OK = "OK" STATUS_ERR = "ERR" -LAST_UPDATED = '_updated' -DATE_CREATED = '_created' -ISSUES = '_issues' -STATUS = '_status' -ERROR = '_error' -ITEMS = '_items' -LINKS = '_links' -ETAG = '_etag' -VERSION = '_version' # field that stores the version number -DELETED = '_deleted' # field to store soft delete status -META = '_meta' +LAST_UPDATED = "_updated" +DATE_CREATED = "_created" +ISSUES = "_issues" +STATUS = "_status" +ERROR = "_error" +ITEMS = "_items" +LINKS = "_links" +ETAG = "_etag" +VERSION = "_version" # field that stores the version number +DELETED = "_deleted" # field to store soft delete status +META = "_meta" INFO = None VALIDATION_ERROR_STATUS = 422 @@ -131,65 +131,66 @@ # field returned on GET requests so we know if we have the latest copy even if # we access a specific version -LATEST_VERSION = '_latest_version' +LATEST_VERSION = "_latest_version" # appended to ID_FIELD, holds the original document id in parallel collection -VERSION_ID_SUFFIX = '_document' -VERSION_DIFF_INCLUDE = [] # always include these fields when diffing +VERSION_ID_SUFFIX = "_document" +VERSION_DIFF_INCLUDE = [] # always include these fields when diffing -API_VERSION = '' -URL_PREFIX = '' -ID_FIELD = '_id' -CACHE_CONTROL = '' +API_VERSION = "" +URL_PREFIX = "" +ID_FIELD = "_id" +CACHE_CONTROL = "" CACHE_EXPIRES = 0 -ITEM_CACHE_CONTROL = '' -X_DOMAINS = None # CORS disabled by default. -X_DOMAINS_RE = None # CORS disabled by default. -X_HEADERS = None # CORS disabled by default. -X_EXPOSE_HEADERS = None # CORS disabled by default. -X_ALLOW_CREDENTIALS = None # CORS disabled by default. -X_MAX_AGE = 21600 # Access-Control-Max-Age when CORS is enabled -HATEOAS = True # HATEOAS enabled by default. -IF_MATCH = True # IF_MATCH (ETag match) enabled by default. -ENFORCE_IF_MATCH = True # ENFORCE_IF_MATCH enabled by default. - -ALLOWED_FILTERS = ['*'] # filtering enabled by default +ITEM_CACHE_CONTROL = "" +X_DOMAINS = None # CORS disabled by default. +X_DOMAINS_RE = None # CORS disabled by default. +X_HEADERS = None # CORS disabled by default. +X_EXPOSE_HEADERS = None # CORS disabled by default. +X_ALLOW_CREDENTIALS = None # CORS disabled by default. +X_MAX_AGE = 21600 # Access-Control-Max-Age when CORS is enabled +HATEOAS = True # HATEOAS enabled by default. +IF_MATCH = True # IF_MATCH (ETag match) enabled by default. +ENFORCE_IF_MATCH = True # ENFORCE_IF_MATCH enabled by default. + +ALLOWED_FILTERS = ["*"] # filtering enabled by default VALIDATE_FILTERS = False -SORTING = True # sorting enabled by default. -JSON_SORT_KEYS = False # json key sorting -RENDERERS = [ - 'eve.render.JSONRenderer', - 'eve.render.XMLRenderer' -] -EMBEDDING = True # embedding enabled by default -PROJECTION = True # projection enabled by default -PAGINATION = True # pagination enabled by default. +SORTING = True # sorting enabled by default. +JSON_SORT_KEYS = False # json key sorting +RENDERERS = ["eve.render.JSONRenderer", "eve.render.XMLRenderer"] +EMBEDDING = True # embedding enabled by default +PROJECTION = True # projection enabled by default +PAGINATION = True # pagination enabled by default. PAGINATION_LIMIT = 50 PAGINATION_DEFAULT = 25 -VERSIONING = False # turn document versioning on or off. -VERSIONS = '_versions' # suffix for parallel collection w/old versions -VERSION_PARAM = 'version' # URL param for specific version of a document. -INTERNAL_RESOURCE = False # resources are public by default. -JSONP_ARGUMENT = None # JSONP disabled by default. -SOFT_DELETE = False # soft delete disabled by default. -SHOW_DELETED_PARAM = 'show_deleted' +VERSIONING = False # turn document versioning on or off. +VERSIONS = "_versions" # suffix for parallel collection w/old versions +VERSION_PARAM = "version" # URL param for specific version of a document. +INTERNAL_RESOURCE = False # resources are public by default. +JSONP_ARGUMENT = None # JSONP disabled by default. +SOFT_DELETE = False # soft delete disabled by default. +SHOW_DELETED_PARAM = "show_deleted" BULK_ENABLED = True -OPLOG = False # oplog is disabled by default. -OPLOG_NAME = 'oplog' # default oplog resource name. -OPLOG_ENDPOINT = None # oplog endpoint is disabled by default. -OPLOG_AUDIT = True # oplog audit enabled by default. -OPLOG_METHODS = ['DELETE', - 'POST', - 'PATCH', - 'PUT'] # oplog logs all operations by default. -OPLOG_CHANGE_METHODS = ['DELETE', - 'PATCH', - 'PUT'] # methods which write changes to the oplog -OPLOG_RETURN_EXTRA_FIELD = False # oplog does not return the 'extra' field. - -RESOURCE_METHODS = ['GET'] -ITEM_METHODS = ['GET'] +OPLOG = False # oplog is disabled by default. +OPLOG_NAME = "oplog" # default oplog resource name. +OPLOG_ENDPOINT = None # oplog endpoint is disabled by default. +OPLOG_AUDIT = True # oplog audit enabled by default. +OPLOG_METHODS = [ + "DELETE", + "POST", + "PATCH", + "PUT", +] # oplog logs all operations by default. +OPLOG_CHANGE_METHODS = [ + "DELETE", + "PATCH", + "PUT", +] # methods which write changes to the oplog +OPLOG_RETURN_EXTRA_FIELD = False # oplog does not return the 'extra' field. + +RESOURCE_METHODS = ["GET"] +ITEM_METHODS = ["GET"] PUBLIC_METHODS = [] ALLOWED_ROLES = [] ALLOWED_READ_ROLES = [] @@ -203,21 +204,21 @@ ITEM_LOOKUP = True ITEM_LOOKUP_FIELD = ID_FIELD ITEM_URL = 'regex("[a-f0-9]{24}")' -UPSERT_ON_PUT = True # insert unexisting documents on PUT. +UPSERT_ON_PUT = True # insert unexisting documents on PUT. MERGE_NESTED_DOCUMENTS = True # use a simple file response format by default EXTENDED_MEDIA_INFO = [] RETURN_MEDIA_AS_BASE64_STRING = True RETURN_MEDIA_AS_URL = False -MEDIA_ENDPOINT = 'media' +MEDIA_ENDPOINT = "media" MEDIA_URL = 'regex("[a-f0-9]{24}")' MEDIA_BASE_URL = None MULTIPART_FORM_FIELDS_AS_JSON = False AUTO_COLLAPSE_MULTI_KEYS = False AUTO_CREATE_LISTS = False -JSON_REQUEST_CONTENT_TYPES = ['application/json'] +JSON_REQUEST_CONTENT_TYPES = ["application/json"] SCHEMA_ENDPOINT = None @@ -228,15 +229,15 @@ BANDWIDTH_SAVER = True # default query parameters -QUERY_WHERE = 'where' -QUERY_PROJECTION = 'projection' -QUERY_SORT = 'sort' -QUERY_PAGE = 'page' -QUERY_MAX_RESULTS = 'max_results' -QUERY_EMBEDDED = 'embedded' -QUERY_AGGREGATION = 'aggregate' - -HEADER_TOTAL_COUNT = 'X-Total-Count' +QUERY_WHERE = "where" +QUERY_PROJECTION = "projection" +QUERY_SORT = "sort" +QUERY_PAGE = "page" +QUERY_MAX_RESULTS = "max_results" +QUERY_EMBEDDED = "embedded" +QUERY_AGGREGATION = "aggregate" + +HEADER_TOTAL_COUNT = "X-Total-Count" OPTIMIZE_PAGINATION_FOR_SPEED = False # user-restricted resource access is disabled by default. @@ -258,11 +259,8 @@ # disallow Mongo's javascript queries as they might be vulnerable to injection # attacks ('ReDoS' especially), are probably too complex for the average API # end-user and finally can seriously impact overall performance. -MONGO_QUERY_BLACKLIST = ['$where', '$regex'] +MONGO_QUERY_BLACKLIST = ["$where", "$regex"] # Explicitly set default write_concern to 'safe' (do regular # aknowledged writes). This is also the current PyMongo/Mongo default setting. -MONGO_WRITE_CONCERN = {'w': 1} -MONGO_OPTIONS = { - 'connect': True, - 'tz_aware': True, -} +MONGO_WRITE_CONCERN = {"w": 1} +MONGO_OPTIONS = {"connect": True, "tz_aware": True} diff --git a/eve/endpoints.py b/eve/endpoints.py index 6820f2c20..923515300 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -52,13 +52,13 @@ def collections_endpoint(**lookup): resource = _resource() response = None method = request.method - if method in ('GET', 'HEAD'): + if method in ("GET", "HEAD"): response = get(resource, lookup) - elif method == 'POST': + elif method == "POST": response = post(resource) - elif method == 'DELETE': + elif method == "DELETE": response = delete(resource, lookup) - elif method == 'OPTIONS': + elif method == "OPTIONS": send_response(resource, response) else: abort(405) @@ -90,15 +90,15 @@ def item_endpoint(**lookup): resource = _resource() response = None method = request.method - if method in ('GET', 'HEAD'): + if method in ("GET", "HEAD"): response = getitem(resource, **lookup) - elif method == 'PATCH': + elif method == "PATCH": response = patch(resource, **lookup) - elif method == 'PUT': + elif method == "PUT": response = put(resource, **lookup) - elif method == 'DELETE': + elif method == "DELETE": response = deleteitem(resource, **lookup) - elif method == 'OPTIONS': + elif method == "OPTIONS": send_response(resource, response) else: abort(405) @@ -106,7 +106,7 @@ def item_endpoint(**lookup): @ratelimit() -@requires_auth('home') +@requires_auth("home") def home_endpoint(): """ Home/API entry point. Will provide links to each available resource @@ -126,26 +126,33 @@ def home_endpoint(): response = {} if config.INFO: info = {} - info['server'] = 'Eve' - info['version'] = eve.__version__ + info["server"] = "Eve" + info["version"] = eve.__version__ if config.API_VERSION: - info['api_version'] = config.API_VERSION + info["api_version"] = config.API_VERSION response[config.INFO] = info if config.HATEOAS: links = [] for resource in config.DOMAIN.keys(): - internal = config.DOMAIN[resource]['internal_resource'] + internal = config.DOMAIN[resource]["internal_resource"] if not resource.endswith(config.VERSIONS): if not bool(internal): - links.append({'href': '%s' % config.URLS[resource], - 'title': '%s' % - config.DOMAIN[resource]['resource_title']}) + links.append( + { + "href": "%s" % config.URLS[resource], + "title": "%s" % config.DOMAIN[resource]["resource_title"], + } + ) if config.SCHEMA_ENDPOINT is not None: - links.append({'href': '%s' % config.SCHEMA_ENDPOINT, - 'title': '%s' % config.SCHEMA_ENDPOINT}) - - response[config.LINKS] = {'child': links} + links.append( + { + "href": "%s" % config.SCHEMA_ENDPOINT, + "title": "%s" % config.SCHEMA_ENDPOINT, + } + ) + + response[config.LINKS] = {"child": links} return send_response(None, (response,)) else: return send_response(None, (response,)) @@ -162,15 +169,16 @@ def error_endpoint(error): headers = error.response.headers response = { config.STATUS: config.STATUS_ERR, - config.ERROR: {'code': error.code, 'message': error.description}} + config.ERROR: {"code": error.code, "message": error.description}, + } return send_response(None, (response, None, None, error.code, headers)) def _resource(): - return request.endpoint.split('|')[0] + return request.endpoint.split("|")[0] -@requires_auth('media') +@requires_auth("media") def media_endpoint(_id): """ This endpoint is active when RETURN_MEDIA_AS_URL is True. It retrieves a media file and streams it to the client. @@ -182,18 +190,18 @@ def media_endpoint(_id): return abort(404) headers = { - 'Last-Modified': date_to_rfc1123(file_.upload_date), - 'Content-Length': file_.length, - 'Accept-Ranges': 'bytes', + "Last-Modified": date_to_rfc1123(file_.upload_date), + "Content-Length": file_.length, + "Accept-Ranges": "bytes", } - range_header = request.headers.get('Range') + range_header = request.headers.get("Range") if range_header: status = 206 size = file_.length try: - m = re.search('(\d+)-(\d*)', range_header) + m = re.search("(\d+)-(\d*)", range_header) begin, end = m.groups() begin = int(begin) end = int(end) @@ -207,17 +215,14 @@ def media_endpoint(_id): file_.seek(begin) data = file_.read(length) - headers['Content-Range'] = 'bytes {0}-{1}/{2}'.format( - begin, - begin + length - 1, - size + headers["Content-Range"] = "bytes {0}-{1}/{2}".format( + begin, begin + length - 1, size ) else: - if_modified_since = weak_date(request.headers.get('If-Modified-Since')) + if_modified_since = weak_date(request.headers.get("If-Modified-Since")) if if_modified_since: if not if_modified_since.tzinfo: - if_modified_since = if_modified_since.replace( - tzinfo=tz_util.utc) + if_modified_since = if_modified_since.replace(tzinfo=tz_util.utc) if if_modified_since > file_.upload_date: return Response(status=304) @@ -230,47 +235,47 @@ def media_endpoint(_id): status=status, headers=headers, mimetype=file_.content_type, - direct_passthrough=True + direct_passthrough=True, ) return response -@requires_auth('resource') +@requires_auth("resource") def schema_item_endpoint(resource): """ This endpoint is active when SCHEMA_ENDPOINT != None. It returns the requested resource's schema definition in JSON format. """ - resource_config = app.config['DOMAIN'].get(resource) - if not resource_config or resource_config.get('internal_resource') is True: + resource_config = app.config["DOMAIN"].get(resource) + if not resource_config or resource_config.get("internal_resource") is True: return abort(404) - return send_response(None, (resource_config['schema'],)) + return send_response(None, (resource_config["schema"],)) -@requires_auth('home') +@requires_auth("home") def schema_collection_endpoint(): """ This endpoint is active when SCHEMA_ENDPOINT != None. It returns the schema definition for all public or request authenticated resources in JSON format. """ schemas = {} - for resource_name, resource_config in app.config['DOMAIN'].items(): + for resource_name, resource_config in app.config["DOMAIN"].items(): # skip versioned shadow collections if resource_name.endswith(config.VERSIONS): continue # skip internal resources - internal = resource_config.get('internal_resource', False) + internal = resource_config.get("internal_resource", False) if internal: continue # skip resources for which request does not have read authorization auth = resource_auth(resource_name) - if auth and request.method not in resource_config['public_methods']: - roles = list(resource_config['allowed_roles']) - roles += resource_config['allowed_read_roles'] + if auth and request.method not in resource_config["public_methods"]: + roles = list(resource_config["allowed_roles"]) + roles += resource_config["allowed_read_roles"] if not auth.authorized(roles, resource_name, request.method): continue # otherwise include this resource in domain wide schema response - schemas[resource_name] = resource_config['schema'] + schemas[resource_name] = resource_config["schema"] return send_response(None, (schemas,)) diff --git a/eve/exceptions.py b/eve/exceptions.py index 9369fba56..230443f8f 100644 --- a/eve/exceptions.py +++ b/eve/exceptions.py @@ -15,9 +15,11 @@ class ConfigException(Exception): """ Raised when errors are found in the configuration settings (usually `settings.py`). """ + pass class SchemaException(ConfigException): """ Raised when errors are found in a field schema definition """ + pass diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 6f0ac8fb6..f4dc33d05 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -22,12 +22,17 @@ import eve from eve import default_settings -from eve.endpoints import collections_endpoint, item_endpoint, home_endpoint, \ - error_endpoint, media_endpoint, schema_collection_endpoint, \ - schema_item_endpoint +from eve.endpoints import ( + collections_endpoint, + item_endpoint, + home_endpoint, + error_endpoint, + media_endpoint, + schema_collection_endpoint, + schema_item_endpoint, +) from eve.exceptions import ConfigException, SchemaException -from eve.io.mongo import Mongo, Validator, GridFSMediaStorage, \ - ensure_mongo_indexes +from eve.io.mongo import Mongo, Validator, GridFSMediaStorage, ensure_mongo_indexes from eve.logging import RequestFilter from eve.utils import api_prefix, extract_key_values @@ -36,14 +41,18 @@ class EveWSGIRequestHandler(WSGIRequestHandler): """ Extend werkzeug request handler to include current Eve version in all responses, which is super-handy for debugging. """ + @property def server_version(self): - return 'Eve/%s ' % eve.__version__ + super(EveWSGIRequestHandler, - self).server_version + return ( + "Eve/%s " % eve.__version__ + + super(EveWSGIRequestHandler, self).server_version + ) class RegexConverter(BaseConverter): """ Extend werkzeug routing by supporting regex for urls/API endpoints """ + def __init__(self, url_map, *items): super(RegexConverter, self).__init__(url_map) self.regex = items[0] @@ -115,16 +124,26 @@ class Eve(Flask, Events): .. versionchanged:: 0.0.4 'auth' argument added to handle authentication classes """ + #: Allowed methods for resource endpoints - supported_resource_methods = ['GET', 'POST', 'DELETE'] + supported_resource_methods = ["GET", "POST", "DELETE"] #: Allowed methods for item endpoints - supported_item_methods = ['GET', 'PATCH', 'DELETE', 'PUT'] - - def __init__(self, import_name=__package__, settings='settings.py', - validator=Validator, data=Mongo, auth=None, redis=None, - url_converters=None, json_encoder=None, - media=GridFSMediaStorage, **kwargs): + supported_item_methods = ["GET", "PATCH", "DELETE", "PUT"] + + def __init__( + self, + import_name=__package__, + settings="settings.py", + validator=Validator, + data=Mongo, + auth=None, + redis=None, + url_converters=None, + json_encoder=None, + media=GridFSMediaStorage, + **kwargs + ): """ Eve main WSGI app is implemented as a Flask subclass. Since we want to be able to launch our API by simply invoking Flask's run() method, we need to enhance our super-class a little bit. @@ -142,7 +161,7 @@ def __init__(self, import_name=__package__, settings='settings.py', self.validate_domain_struct() # enable regex routing - self.url_map.converters['regex'] = RegexConverter + self.url_map.converters["regex"] = RegexConverter # optional url_converters and json encoder if url_converters: @@ -164,7 +183,7 @@ def __init__(self, import_name=__package__, settings='settings.py', self._init_media_endpoint() self._init_schema_endpoint() - if self.config['OPLOG'] is True: + if self.config["OPLOG"] is True: self._init_oplog() # validate and set defaults for each resource @@ -173,7 +192,7 @@ def __init__(self, import_name=__package__, settings='settings.py', # further insertion of versioned resources do not # cause a RuntimeError due to the change of size of # the dict - domain_copy = copy.deepcopy(self.config['DOMAIN']) + domain_copy = copy.deepcopy(self.config["DOMAIN"]) for resource, settings in domain_copy.items(): self.register_resource(resource, settings) @@ -199,7 +218,7 @@ def run(self, host=None, port=None, debug=None, **options): :func:`werkzeug.serving.run_simple` for more information. """ - options.setdefault('request_handler', EveWSGIRequestHandler) + options.setdefault("request_handler", EveWSGIRequestHandler) super(Eve, self).run(host, port, debug, **options) def load_config(self): @@ -221,7 +240,7 @@ def load_config(self): """ # load defaults - self.config.from_object('eve.default_settings') + self.config.from_object("eve.default_settings") # overwrite the defaults with custom user settings if isinstance(self.settings, dict): @@ -230,6 +249,7 @@ def load_config(self): if os.path.isabs(self.settings): pyfile = self.settings else: + def find_settings_file(file_name): # check if we can locate the file from sys.argv[0] abspath = os.path.abspath(os.path.dirname(sys.argv[0])) @@ -247,11 +267,11 @@ def find_settings_file(file_name): # try to load file from environment variable or settings.py pyfile = find_settings_file( - os.environ.get('EVE_SETTINGS') or self.settings + os.environ.get("EVE_SETTINGS") or self.settings ) if not pyfile: - raise IOError('Could not load settings.') + raise IOError("Could not load settings.") try: self.config.from_pyfile(pyfile) @@ -259,9 +279,7 @@ def find_settings_file(file_name): raise # flask-pymongo compatibility - self.config['MONGO_CONNECT'] = self.config['MONGO_OPTIONS'].get( - 'connect', True - ) + self.config["MONGO_CONNECT"] = self.config["MONGO_OPTIONS"].get("connect", True) self.check_deprecated_features() @@ -273,21 +291,20 @@ def deprecated_renderers_settings(): """ Checks if JSON or XML setting is still being used instead of RENDERERS and if so, composes new settings. """ - msg = '{} setting is deprecated and will be removed' \ - ' in future release. Please use RENDERERS instead.' + msg = "{} setting is deprecated and will be removed" " in future release. Please use RENDERERS instead." - if 'JSON' in self.config or 'XML' in self.config: - self.config['RENDERERS'] = default_settings.RENDERERS.copy() + if "JSON" in self.config or "XML" in self.config: + self.config["RENDERERS"] = default_settings.RENDERERS.copy() - if 'JSON' in self.config: - warnings.warn(msg.format('JSON')) - if not self.config['JSON']: - self.config['RENDERERS'].remove('eve.render.JSONRenderer') + if "JSON" in self.config: + warnings.warn(msg.format("JSON")) + if not self.config["JSON"]: + self.config["RENDERERS"].remove("eve.render.JSONRenderer") - if 'XML' in self.config: - warnings.warn(msg.format('XML')) - if not self.config['XML']: - self.config['RENDERERS'].remove('eve.render.XMLRenderer') + if "XML" in self.config: + warnings.warn(msg.format("XML")) + if not self.config["XML"]: + self.config["RENDERERS"].remove("eve.render.XMLRenderer") deprecated_renderers_settings() @@ -296,11 +313,11 @@ def validate_domain_struct(self): requirements. """ try: - domain = self.config['DOMAIN'] + domain = self.config["DOMAIN"] except: - raise ConfigException('DOMAIN dictionary missing or wrong.') + raise ConfigException("DOMAIN dictionary missing or wrong.") if not isinstance(domain, dict): - raise ConfigException('DOMAIN must be a dict.') + raise ConfigException("DOMAIN must be a dict.") def validate_config(self): """ Makes sure that REST methods expressed in the configuration @@ -320,17 +337,19 @@ def validate_config(self): Support for DELETE resource method. """ # make sure that global resource methods are supported. - self.validate_methods(self.supported_resource_methods, - self.config.get('RESOURCE_METHODS'), - 'resource') + self.validate_methods( + self.supported_resource_methods, + self.config.get("RESOURCE_METHODS"), + "resource", + ) # make sure that global item methods are supported. - self.validate_methods(self.supported_item_methods, - self.config.get('ITEM_METHODS'), - 'item') + self.validate_methods( + self.supported_item_methods, self.config.get("ITEM_METHODS"), "item" + ) # make sure that individual resource/item methods are supported. - for resource, settings in self.config['DOMAIN'].items(): + for resource, settings in self.config["DOMAIN"].items(): self._validate_resource_settings(resource, settings) def _validate_resource_settings(self, resource, settings): @@ -344,34 +363,44 @@ def _validate_resource_settings(self, resource, settings): .. versionadded:: 0.2 """ - self.validate_methods(self.supported_resource_methods, - settings['resource_methods'], - '[%s] resource ' % resource) - self.validate_methods(self.supported_item_methods, - settings['item_methods'], - '[%s] item ' % resource) + self.validate_methods( + self.supported_resource_methods, + settings["resource_methods"], + "[%s] resource " % resource, + ) + self.validate_methods( + self.supported_item_methods, + settings["item_methods"], + "[%s] item " % resource, + ) # while a resource schema is optional for read-only access, # it is mandatory for write-access to resource/items. - if 'POST' in settings['resource_methods'] or \ - 'PATCH' in settings['item_methods']: - if len(settings['schema']) == 0: - raise ConfigException('A resource schema must be provided ' - 'when POST or PATCH methods are allowed ' - 'for a resource [%s].' % resource) - - self.validate_roles('allowed_roles', settings, resource) - self.validate_roles('allowed_read_roles', settings, resource) - self.validate_roles('allowed_write_roles', settings, resource) - self.validate_roles('allowed_item_roles', settings, resource) - self.validate_roles('allowed_item_read_roles', settings, resource) - self.validate_roles('allowed_item_write_roles', settings, resource) - - if settings['auth_field'] == settings['id_field']: - raise ConfigException('"%s": auth_field cannot be set to id_field ' - '(%s)' % (resource, settings['id_field'])) - - self.validate_schema(resource, settings['schema']) + if ( + "POST" in settings["resource_methods"] + or "PATCH" in settings["item_methods"] + ): + if len(settings["schema"]) == 0: + raise ConfigException( + "A resource schema must be provided " + "when POST or PATCH methods are allowed " + "for a resource [%s]." % resource + ) + + self.validate_roles("allowed_roles", settings, resource) + self.validate_roles("allowed_read_roles", settings, resource) + self.validate_roles("allowed_write_roles", settings, resource) + self.validate_roles("allowed_item_roles", settings, resource) + self.validate_roles("allowed_item_read_roles", settings, resource) + self.validate_roles("allowed_item_write_roles", settings, resource) + + if settings["auth_field"] == settings["id_field"]: + raise ConfigException( + '"%s": auth_field cannot be set to id_field ' + "(%s)" % (resource, settings["id_field"]) + ) + + self.validate_schema(resource, settings["schema"]) def validate_roles(self, directive, candidate, resource): """ Validates that user role directives are syntactically and formally @@ -387,8 +416,7 @@ def validate_roles(self, directive, candidate, resource): """ roles = candidate[directive] if not isinstance(roles, list): - raise ConfigException("'%s' must be list" - "[%s]." % (directive, resource)) + raise ConfigException("'%s' must be list" "[%s]." % (directive, resource)) def validate_methods(self, allowed, proposed, item): """ Compares allowed and proposed methods, raising a `ConfigException` @@ -401,10 +429,10 @@ def validate_methods(self, allowed, proposed, item): """ diff = set(proposed) - set(allowed) if diff: - raise ConfigException('Unallowed %s method(s): %s. ' - 'Supported: %s' % - (item, ', '.join(diff), - ', '.join(allowed))) + raise ConfigException( + "Unallowed %s method(s): %s. " + "Supported: %s" % (item, ", ".join(diff), ", ".join(allowed)) + ) def validate_schema(self, resource, schema): """ Validates a resource schema. @@ -440,59 +468,67 @@ def validate_schema(self, resource, schema): Now collecting offending items in a list and inserting results into the exception message. """ + def validate_field_name(field): - forbidden = ['$', '.'] + forbidden = ["$", "."] if any(x in field for x in forbidden): raise SchemaException( - "Field '%s' cannot contain any of the following: '%s'." % - (field, ', '.join(forbidden))) + "Field '%s' cannot contain any of the following: '%s'." + % (field, ", ".join(forbidden)) + ) - resource_settings = self.config['DOMAIN'][resource] + resource_settings = self.config["DOMAIN"][resource] # ensure automatically handled fields aren't defined fields = [eve.DATE_CREATED, eve.LAST_UPDATED, eve.ETAG] - if resource_settings['versioning'] is True: + if resource_settings["versioning"] is True: fields += [ - self.config['VERSION'], - self.config['LATEST_VERSION'], - resource_settings['id_field'] + - self.config['VERSION_ID_SUFFIX']] - if resource_settings['soft_delete'] is True: - fields += [self.config['DELETED']] + self.config["VERSION"], + self.config["LATEST_VERSION"], + resource_settings["id_field"] + self.config["VERSION_ID_SUFFIX"], + ] + if resource_settings["soft_delete"] is True: + fields += [self.config["DELETED"]] offenders = [] for field in fields: if field in schema: offenders.append(field) if offenders: - raise SchemaException('field(s) "%s" not allowed in "%s" schema ' - '(they will be handled automatically).' - % (', '.join(offenders), resource)) + raise SchemaException( + 'field(s) "%s" not allowed in "%s" schema ' + "(they will be handled automatically)." + % (", ".join(offenders), resource) + ) if not isinstance(schema, dict): return for field, ruleset in schema.items(): validate_field_name(field) - if isinstance(ruleset, dict) and 'dict' in ruleset.get('type', ''): - for field in ruleset.get('schema', {}).keys(): + if isinstance(ruleset, dict) and "dict" in ruleset.get("type", ""): + for field in ruleset.get("schema", {}).keys(): validate_field_name(field) # check data_relation rules - if 'data_relation' in ruleset: - if 'resource' not in ruleset['data_relation']: - raise SchemaException("'resource' key is mandatory for " - "the 'data_relation' rule in " - "'%s: %s'" % (resource, field)) - if ruleset['data_relation'].get('embeddable', False): + if "data_relation" in ruleset: + if "resource" not in ruleset["data_relation"]: + raise SchemaException( + "'resource' key is mandatory for " + "the 'data_relation' rule in " + "'%s: %s'" % (resource, field) + ) + if ruleset["data_relation"].get("embeddable", False): # special care for data_relations with a version - value_field = ruleset['data_relation']['field'] - if ruleset['data_relation'].get('version', False): - if 'schema' not in ruleset or \ - value_field not in ruleset['schema'] or \ - 'type' not in ruleset['schema'][value_field]: + value_field = ruleset["data_relation"]["field"] + if ruleset["data_relation"].get("version", False): + if ( + "schema" not in ruleset + or value_field not in ruleset["schema"] + or "type" not in ruleset["schema"][value_field] + ): raise SchemaException( "Must defined type for '%s' in schema when " "declaring an embedded data_relation with" @@ -556,7 +592,7 @@ def set_defaults(self): `item_title` default value. """ - for resource, settings in self.config['DOMAIN'].items(): + for resource, settings in self.config["DOMAIN"].items(): self._set_resource_defaults(resource, settings) def _set_resource_defaults(self, resource, settings): @@ -587,70 +623,61 @@ def _set_resource_defaults(self, resource, settings): 'embedded_fields'. Support for endpoint-level authentication classes. """ - settings.setdefault('url', resource) - settings.setdefault('resource_methods', - self.config['RESOURCE_METHODS']) - settings.setdefault('public_methods', - self.config['PUBLIC_METHODS']) - settings.setdefault('allowed_roles', self.config['ALLOWED_ROLES']) - settings.setdefault('allowed_read_roles', - self.config['ALLOWED_READ_ROLES']) - settings.setdefault('allowed_write_roles', - self.config['ALLOWED_WRITE_ROLES']) - settings.setdefault('cache_control', self.config['CACHE_CONTROL']) - settings.setdefault('cache_expires', self.config['CACHE_EXPIRES']) - - settings.setdefault('id_field', self.config['ID_FIELD']) - settings.setdefault('item_lookup_field', - self.config['ITEM_LOOKUP_FIELD']) - settings.setdefault('item_url', self.config['ITEM_URL']) - settings.setdefault('resource_title', settings['url']) - settings.setdefault('item_title', - resource.rstrip('s').capitalize()) - settings.setdefault('item_lookup', self.config['ITEM_LOOKUP']) - settings.setdefault('public_item_methods', - self.config['PUBLIC_ITEM_METHODS']) - settings.setdefault('allowed_item_roles', - self.config['ALLOWED_ITEM_ROLES']) - settings.setdefault('allowed_item_read_roles', - self.config['ALLOWED_ITEM_READ_ROLES']) - settings.setdefault('allowed_item_write_roles', - self.config['ALLOWED_ITEM_WRITE_ROLES']) - settings.setdefault('allowed_filters', - self.config['ALLOWED_FILTERS']) - settings.setdefault('sorting', self.config['SORTING']) - settings.setdefault('embedding', self.config['EMBEDDING']) - settings.setdefault('embedded_fields', []) - settings.setdefault('pagination', self.config['PAGINATION']) - settings.setdefault('projection', self.config['PROJECTION']) - settings.setdefault('versioning', self.config['VERSIONING']) - settings.setdefault('soft_delete', self.config['SOFT_DELETE']) - settings.setdefault('bulk_enabled', self.config['BULK_ENABLED']) - settings.setdefault('internal_resource', - self.config['INTERNAL_RESOURCE']) - settings.setdefault('etag_ignore_fields', None) + settings.setdefault("url", resource) + settings.setdefault("resource_methods", self.config["RESOURCE_METHODS"]) + settings.setdefault("public_methods", self.config["PUBLIC_METHODS"]) + settings.setdefault("allowed_roles", self.config["ALLOWED_ROLES"]) + settings.setdefault("allowed_read_roles", self.config["ALLOWED_READ_ROLES"]) + settings.setdefault("allowed_write_roles", self.config["ALLOWED_WRITE_ROLES"]) + settings.setdefault("cache_control", self.config["CACHE_CONTROL"]) + settings.setdefault("cache_expires", self.config["CACHE_EXPIRES"]) + + settings.setdefault("id_field", self.config["ID_FIELD"]) + settings.setdefault("item_lookup_field", self.config["ITEM_LOOKUP_FIELD"]) + settings.setdefault("item_url", self.config["ITEM_URL"]) + settings.setdefault("resource_title", settings["url"]) + settings.setdefault("item_title", resource.rstrip("s").capitalize()) + settings.setdefault("item_lookup", self.config["ITEM_LOOKUP"]) + settings.setdefault("public_item_methods", self.config["PUBLIC_ITEM_METHODS"]) + settings.setdefault("allowed_item_roles", self.config["ALLOWED_ITEM_ROLES"]) + settings.setdefault( + "allowed_item_read_roles", self.config["ALLOWED_ITEM_READ_ROLES"] + ) + settings.setdefault( + "allowed_item_write_roles", self.config["ALLOWED_ITEM_WRITE_ROLES"] + ) + settings.setdefault("allowed_filters", self.config["ALLOWED_FILTERS"]) + settings.setdefault("sorting", self.config["SORTING"]) + settings.setdefault("embedding", self.config["EMBEDDING"]) + settings.setdefault("embedded_fields", []) + settings.setdefault("pagination", self.config["PAGINATION"]) + settings.setdefault("projection", self.config["PROJECTION"]) + settings.setdefault("versioning", self.config["VERSIONING"]) + settings.setdefault("soft_delete", self.config["SOFT_DELETE"]) + settings.setdefault("bulk_enabled", self.config["BULK_ENABLED"]) + settings.setdefault("internal_resource", self.config["INTERNAL_RESOURCE"]) + settings.setdefault("etag_ignore_fields", None) # TODO make sure that this we really need the test below - if settings['item_lookup']: - item_methods = self.config['ITEM_METHODS'] + if settings["item_lookup"]: + item_methods = self.config["ITEM_METHODS"] else: item_methods = eve.ITEM_METHODS - settings.setdefault('item_methods', item_methods) - settings.setdefault('auth_field', - self.config['AUTH_FIELD']) - settings.setdefault('allow_unknown', self.config['ALLOW_UNKNOWN']) - settings.setdefault('extra_response_fields', - self.config['EXTRA_RESPONSE_FIELDS']) - settings.setdefault('mongo_write_concern', - self.config['MONGO_WRITE_CONCERN']) - settings.setdefault('mongo_indexes', {}) - settings.setdefault('hateoas', - self.config['HATEOAS']) - settings.setdefault('authentication', self.auth if self.auth else None) - settings.setdefault('merge_nested_documents', - self.config['MERGE_NESTED_DOCUMENTS']) + settings.setdefault("item_methods", item_methods) + settings.setdefault("auth_field", self.config["AUTH_FIELD"]) + settings.setdefault("allow_unknown", self.config["ALLOW_UNKNOWN"]) + settings.setdefault( + "extra_response_fields", self.config["EXTRA_RESPONSE_FIELDS"] + ) + settings.setdefault("mongo_write_concern", self.config["MONGO_WRITE_CONCERN"]) + settings.setdefault("mongo_indexes", {}) + settings.setdefault("hateoas", self.config["HATEOAS"]) + settings.setdefault("authentication", self.auth if self.auth else None) + settings.setdefault( + "merge_nested_documents", self.config["MERGE_NESTED_DOCUMENTS"] + ) # empty schemas are allowed for read-only access to resources - schema = settings.setdefault('schema', {}) - self.set_schema_defaults(schema, settings['id_field']) + schema = settings.setdefault("schema", {}) + self.set_schema_defaults(schema, settings["id_field"]) self._set_resource_datasource(resource, schema, settings) @@ -660,22 +687,22 @@ def _set_resource_datasource(self, resource, schema, settings): .. versionadded:: 0.7 """ - settings.setdefault('datasource', {}) + settings.setdefault("datasource", {}) - ds = settings['datasource'] - ds.setdefault('source', resource) - ds.setdefault('filter', None) - ds.setdefault('default_sort', None) + ds = settings["datasource"] + ds.setdefault("source", resource) + ds.setdefault("filter", None) + ds.setdefault("default_sort", None) self._set_resource_projection(ds, schema, settings) - aggregation = ds.setdefault('aggregation', None) + aggregation = ds.setdefault("aggregation", None) if aggregation: - aggregation.setdefault('options', {}) + aggregation.setdefault("options", {}) # endpoints serving aggregation queries are read-only and do not # support item lookup. - settings['resource_methods'] = ['GET'] - settings['item_lookup'] = False + settings["resource_methods"] = ["GET"] + settings["item_lookup"] = False def _set_resource_projection(self, ds, schema, settings): """ Set datasource projection for a resource @@ -687,59 +714,71 @@ def _set_resource_projection(self, ds, schema, settings): .. versionadded:: 0.6.2 """ # get existing or empty projection setting - projection = ds.get('projection', {}) + projection = ds.get("projection", {}) # If exclusion projections are defined, they are use for # concealing fields (rather than actual mongo exlusions). # If inclusion projections are defined, exclusion projections are # just ignored. # Enhance the projection with automatic fields. - if len(schema) and settings['allow_unknown'] is False: - inclusion_projection = dict([(k, v) for k, v in projection.items() - if v == 1]) - exclusion_projection = dict([(k, v) for k, v in projection.items() - if v == 0]) + if len(schema) and settings["allow_unknown"] is False: + inclusion_projection = dict( + [(k, v) for k, v in projection.items() if v == 1] + ) + exclusion_projection = dict( + [(k, v) for k, v in projection.items() if v == 0] + ) # if inclusion project is empty, add all fields not excluded if not inclusion_projection: projection.update( - dict((field, 1) for (field) in schema - if field not in exclusion_projection)) + dict( + (field, 1) + for (field) in schema + if field not in exclusion_projection + ) + ) # enable retrieval of actual schema fields only. Eventual db # fields not included in the schema won't be returned. # despite projection, automatic fields are always included. - projection[settings['id_field']] = 1 - projection[self.config['LAST_UPDATED']] = 1 - projection[self.config['DATE_CREATED']] = 1 - projection[self.config['ETAG']] = 1 - if settings['versioning'] is True: - projection[self.config['VERSION']] = 1 - projection[ - settings['id_field'] + - self.config['VERSION_ID_SUFFIX']] = 1 + projection[settings["id_field"]] = 1 + projection[self.config["LAST_UPDATED"]] = 1 + projection[self.config["DATE_CREATED"]] = 1 + projection[self.config["ETAG"]] = 1 + if settings["versioning"] is True: + projection[self.config["VERSION"]] = 1 + projection[settings["id_field"] + self.config["VERSION_ID_SUFFIX"]] = 1 - ds.setdefault('projection', projection) + ds.setdefault("projection", projection) - if settings['soft_delete'] is True and projection: - projection[self.config['DELETED']] = 1 + if settings["soft_delete"] is True and projection: + projection[self.config["DELETED"]] = 1 # set projection and projection is always a dictionary - ds['projection'] = projection + ds["projection"] = projection # list of all media fields for the resource if isinstance(schema, dict): - settings['_media'] = [field for field, definition in schema.items() - if isinstance(definition, dict) and - (definition.get('type') == 'media' or - (definition.get('type') == 'list' and - definition.get('schema', {}).get('type') == - 'media'))] + settings["_media"] = [ + field + for field, definition in schema.items() + if isinstance(definition, dict) + and ( + definition.get("type") == "media" + or ( + definition.get("type") == "list" + and definition.get("schema", {}).get("type") == "media" + ) + ) + ] else: - settings['_media'] = [] + settings["_media"] = [] - if settings['_media'] and not self.media: - raise ConfigException('A media storage class of type ' - ' eve.io.media.MediaStorage must be defined ' - 'for "media" fields to be properly stored.') + if settings["_media"] and not self.media: + raise ConfigException( + "A media storage class of type " + " eve.io.media.MediaStorage must be defined " + 'for "media" fields to be properly stored.' + ) def set_schema_defaults(self, schema, id_field): """ When not provided, fills individual schema settings with default @@ -764,12 +803,12 @@ def set_schema_defaults(self, schema, id_field): # avoids a performance hit (with 'unique' rule set, we would # end up with an extra db loopback on every insert). if isinstance(schema, dict): - schema.setdefault(id_field, {'type': 'objectid'}) + schema.setdefault(id_field, {"type": "objectid"}) # set default 'field' value for all 'data_relation' rulesets, however # nested - for data_relation in list(extract_key_values('data_relation', schema)): - data_relation.setdefault('field', id_field) + for data_relation in list(extract_key_values("data_relation", schema)): + data_relation.setdefault("field", id_field) @property def api_prefix(self): @@ -777,8 +816,7 @@ def api_prefix(self): .. versionadded:: 0.2 """ - return api_prefix(self.config['URL_PREFIX'], - self.config['API_VERSION']) + return api_prefix(self.config["URL_PREFIX"], self.config["API_VERSION"]) def _add_resource_url_rules(self, resource, settings): """ Builds the API url map for one resource. Methods are enabled for @@ -790,53 +828,69 @@ def _add_resource_url_rules(self, resource, settings): .. versionadded:: 0.2 """ - self.config['SOURCES'][resource] = settings['datasource'] + self.config["SOURCES"][resource] = settings["datasource"] - if settings['internal_resource']: + if settings["internal_resource"]: return - url = '%s/%s' % (self.api_prefix, settings['url']) + url = "%s/%s" % (self.api_prefix, settings["url"]) - pretty_url = settings['url'] - if '<' in pretty_url: - pretty_url = pretty_url[:pretty_url.index('<') + 1] + \ - pretty_url[pretty_url.rindex(':') + 1:] - self.config['URLS'][resource] = pretty_url + pretty_url = settings["url"] + if "<" in pretty_url: + pretty_url = ( + pretty_url[: pretty_url.index("<") + 1] + + pretty_url[pretty_url.rindex(":") + 1 :] + ) + self.config["URLS"][resource] = pretty_url # resource endpoint endpoint = resource + "|resource" - self.add_url_rule(url, endpoint, view_func=collections_endpoint, - methods=settings['resource_methods'] + ['OPTIONS']) + self.add_url_rule( + url, + endpoint, + view_func=collections_endpoint, + methods=settings["resource_methods"] + ["OPTIONS"], + ) # item endpoint - if settings['item_lookup']: - item_url = '%s/<%s:%s>' % (url, settings['item_url'], - settings['item_lookup_field']) + if settings["item_lookup"]: + item_url = "%s/<%s:%s>" % ( + url, + settings["item_url"], + settings["item_lookup_field"], + ) endpoint = resource + "|item_lookup" - self.add_url_rule(item_url, endpoint, - view_func=item_endpoint, - methods=settings['item_methods'] + ['OPTIONS']) - if 'PATCH' in settings['item_methods']: + self.add_url_rule( + item_url, + endpoint, + view_func=item_endpoint, + methods=settings["item_methods"] + ["OPTIONS"], + ) + if "PATCH" in settings["item_methods"]: # support for POST with X-HTTP-Method-Override header for # clients not supporting PATCH. Also see item_endpoint() in # endpoints.py endpoint = resource + "|item_post_override" - self.add_url_rule(item_url, endpoint, view_func=item_endpoint, - methods=['POST']) + self.add_url_rule( + item_url, endpoint, view_func=item_endpoint, methods=["POST"] + ) # also enable an alternative lookup/endpoint if allowed - lookup = settings.get('additional_lookup') + lookup = settings.get("additional_lookup") if lookup: - l_type = settings['schema'][lookup['field']]['type'] - if l_type == 'integer': - item_url = '%s/' % (url, lookup['field']) + l_type = settings["schema"][lookup["field"]]["type"] + if l_type == "integer": + item_url = "%s/" % (url, lookup["field"]) else: - item_url = '%s/<%s:%s>' % (url, lookup['url'], - lookup['field']) + item_url = "%s/<%s:%s>" % (url, lookup["url"], lookup["field"]) endpoint = resource + "|item_additional_lookup" - self.add_url_rule(item_url, endpoint, view_func=item_endpoint, - methods=['GET', 'OPTIONS']) + self.add_url_rule( + item_url, + endpoint, + view_func=item_endpoint, + methods=["GET", "OPTIONS"], + ) def _init_url_rules(self): """ Builds the API url map. Methods are enabled for each mapped @@ -869,8 +923,8 @@ def _init_url_rules(self): Support for API_VERSION as an endpoint prefix. """ # helpers - self.config['URLS'] = {} # maps resources to urls - self.config['SOURCES'] = {} # maps resources to their datasources + self.config["URLS"] = {} # maps resources to urls + self.config["SOURCES"] = {} # maps resources to their datasources # we choose not to care about trailing slashes at all. # Both '/resource/' and '/resource' will work, same with @@ -878,8 +932,12 @@ def _init_url_rules(self): self.url_map.strict_slashes = False # home page (API entry point) - self.add_url_rule('%s/' % self.api_prefix, 'home', - view_func=home_endpoint, methods=['GET', 'OPTIONS']) + self.add_url_rule( + "%s/" % self.api_prefix, + "home", + view_func=home_endpoint, + methods=["GET", "OPTIONS"], + ) def register_resource(self, resource, settings): """ Registers new resource to the domain. @@ -904,7 +962,7 @@ def register_resource(self, resource, settings): # this line only makes sense when we call this function outside of the # standard Eve setup routine, but it doesn't hurt to still call it - self.config['DOMAIN'][resource] = settings + self.config["DOMAIN"][resource] = settings # set up resource self._set_resource_defaults(resource, settings) @@ -912,31 +970,32 @@ def register_resource(self, resource, settings): self._add_resource_url_rules(resource, settings) # add rules for version control collections if appropriate - if settings['versioning'] is True: - versioned_resource = resource + self.config['VERSIONS'] - self.config['DOMAIN'][versioned_resource] = \ - copy.deepcopy(self.config['DOMAIN'][resource]) - self.config['DOMAIN'][versioned_resource]['datasource']['source'] \ - += self.config['VERSIONS'] - self.config['SOURCES'][versioned_resource] = \ - copy.deepcopy(self.config['SOURCES'][resource]) - self.config['SOURCES'][versioned_resource]['source'] += \ - self.config['VERSIONS'] + if settings["versioning"] is True: + versioned_resource = resource + self.config["VERSIONS"] + self.config["DOMAIN"][versioned_resource] = copy.deepcopy( + self.config["DOMAIN"][resource] + ) + self.config["DOMAIN"][versioned_resource]["datasource"][ + "source" + ] += self.config["VERSIONS"] + self.config["SOURCES"][versioned_resource] = copy.deepcopy( + self.config["SOURCES"][resource] + ) + self.config["SOURCES"][versioned_resource]["source"] += self.config[ + "VERSIONS" + ] # the new versioned resource also needs URL rules self._add_resource_url_rules( - versioned_resource, - self.config['DOMAIN'][versioned_resource] + versioned_resource, self.config["DOMAIN"][versioned_resource] ) # create the mongo db indexes ensure_mongo_indexes(self, resource) # flask-pymongo compatibility. - if 'MONGO_OPTIONS' in self.config['DOMAIN']: - connect = self.config['DOMAIN']['MONGO_OPTIONS'].get( - 'connect', True - ) - self.config['DOMAIN']['MONGO_CONNECT'] = connect + if "MONGO_OPTIONS" in self.config["DOMAIN"]: + connect = self.config["DOMAIN"]["MONGO_OPTIONS"].get("connect", True) + self.config["DOMAIN"]["MONGO_CONNECT"] = connect def register_error_handlers(self): """ Register custom error handlers so we make sure that all errors @@ -949,7 +1008,7 @@ def register_error_handlers(self): .. versionadded:: 0.4 """ - for code in self.config['STANDARD_ERRORS']: + for code in self.config["STANDARD_ERRORS"]: self.register_error_handler(code, error_endpoint) def _init_oplog(self): @@ -961,74 +1020,71 @@ def _init_oplog(self): .. versionadded:: 0.5 """ name, endpoint, audit, extra = ( - self.config['OPLOG_NAME'], - self.config['OPLOG_ENDPOINT'], - self.config['OPLOG_AUDIT'], - self.config['OPLOG_RETURN_EXTRA_FIELD'] + self.config["OPLOG_NAME"], + self.config["OPLOG_ENDPOINT"], + self.config["OPLOG_AUDIT"], + self.config["OPLOG_RETURN_EXTRA_FIELD"], ) - settings = self.config['DOMAIN'].setdefault(name, {}) + settings = self.config["DOMAIN"].setdefault(name, {}) - settings.setdefault('datasource', {'source': name}) + settings.setdefault("datasource", {"source": name}) # this endpoint is always read-only - settings['resource_methods'] = ['GET'] - settings['item_methods'] = ['GET'] + settings["resource_methods"] = ["GET"] + settings["item_methods"] = ["GET"] if endpoint: - settings.setdefault('url', endpoint) - settings['internal_resource'] = False + settings.setdefault("url", endpoint) + settings["internal_resource"] = False else: # make it an internal resource - settings['url'] = name - settings['internal_resource'] = True + settings["url"] = name + settings["internal_resource"] = True # schema is also fixed. it is needed because otherwise we # would end up exposing the AUTH_FIELD when User-Restricted- # Resource-Access is enabled. - settings['schema'] = { - 'r': {}, - 'o': {}, - 'i': {}, - } + settings["schema"] = {"r": {}, "o": {}, "i": {}} if extra: - settings['schema'].update( - {'extra': {}} - ) + settings["schema"].update({"extra": {}}) if audit: - settings['schema'].update( - { - 'ip': {}, - 'c': {}, - 'u': {}, - } - ) + settings["schema"].update({"ip": {}, "c": {}, "u": {}}) def _init_media_endpoint(self): - endpoint = self.config['MEDIA_ENDPOINT'] + endpoint = self.config["MEDIA_ENDPOINT"] if endpoint: - media_url = '%s/%s/<%s:_id>' % (self.api_prefix, - endpoint, - self.config['MEDIA_URL']) - self.add_url_rule(media_url, 'media', - view_func=media_endpoint, methods=['GET']) + media_url = "%s/%s/<%s:_id>" % ( + self.api_prefix, + endpoint, + self.config["MEDIA_URL"], + ) + self.add_url_rule( + media_url, "media", view_func=media_endpoint, methods=["GET"] + ) def _init_schema_endpoint(self): """Configures the schema endpoint if set in configuration. """ - endpoint = self.config['SCHEMA_ENDPOINT'] + endpoint = self.config["SCHEMA_ENDPOINT"] if endpoint: - schema_url = '%s/%s' % (self.api_prefix, endpoint) + schema_url = "%s/%s" % (self.api_prefix, endpoint) # add schema collections url - self.add_url_rule(schema_url, 'schema_collection', - view_func=schema_collection_endpoint, - methods=['GET', 'OPTIONS']) + self.add_url_rule( + schema_url, + "schema_collection", + view_func=schema_collection_endpoint, + methods=["GET", "OPTIONS"], + ) # add schema item url - self.add_url_rule(schema_url + '/', 'schema_item', - view_func=schema_item_endpoint, - methods=['GET', 'OPTIONS']) + self.add_url_rule( + schema_url + "/", + "schema_item", + view_func=schema_item_endpoint, + methods=["GET", "OPTIONS"], + ) def __call__(self, environ, start_response): """ If HTTP_X_METHOD_OVERRIDE is included with the request and method @@ -1036,8 +1092,8 @@ def __call__(self, environ, start_response): as the request method, so normal routing and method validation can be performed. """ - if self.config['ALLOW_OVERRIDE_HTTP_METHOD']: - environ['REQUEST_METHOD'] = environ.get( - 'HTTP_X_HTTP_METHOD_OVERRIDE', - environ['REQUEST_METHOD']).upper() + if self.config["ALLOW_OVERRIDE_HTTP_METHOD"]: + environ["REQUEST_METHOD"] = environ.get( + "HTTP_X_HTTP_METHOD_OVERRIDE", environ["REQUEST_METHOD"] + ).upper() return super(Eve, self).__call__(environ, start_response) diff --git a/eve/io/base.py b/eve/io/base.py index 3dac0506e..3b01adade 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -22,6 +22,7 @@ class BaseJSONEncoder(json.JSONEncoder): """ Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. """ + def default(self, obj): if isinstance(obj, datetime.datetime): # convert any datetime to RFC 1123 format @@ -43,12 +44,15 @@ class ConnectionException(Exception): :param driver_exception: the original exception raised by the source db driver """ + def __init__(self, driver_exception=None): self.driver_exception = driver_exception def __str__(self): - msg = ("Error initializing the driver. Make sure the database server" - "is running. ") + msg = ( + "Error initializing the driver. Make sure the database server" + "is running. " + ) if self.driver_exception: msg += "Driver exception: %s" % repr(self.driver_exception) return msg @@ -151,8 +155,14 @@ def aggregate(self, resource, pipeline, options): """ raise NotImplementedError - def find_one(self, resource, req, check_auth_value=True, - force_auth_field_projection=False, **lookup): + def find_one( + self, + resource, + req, + check_auth_value=True, + force_auth_field_projection=False, + **lookup + ): """ Retrieves a single document/record. Consumed when a request hits an item endpoint (`/people/id/`). @@ -344,16 +354,22 @@ def datasource(self, resource): """ dsource = config.SOURCES[resource] - source = copy(dsource['source']) - filter_ = copy(dsource['filter']) - sort = copy(dsource['default_sort']) - projection = copy(dsource['projection']) - - return source, filter_, projection, sort, - - def _datasource_ex(self, resource, query=None, client_projection=None, - client_sort=None, check_auth_value=True, - force_auth_field_projection=False): + source = copy(dsource["source"]) + filter_ = copy(dsource["filter"]) + sort = copy(dsource["default_sort"]) + projection = copy(dsource["projection"]) + + return source, filter_, projection, sort + + def _datasource_ex( + self, + resource, + query=None, + client_projection=None, + client_sort=None, + check_auth_value=True, + force_auth_field_projection=False, + ): """ Returns both db collection and exact query (base filter included) to which an API resource refers to. @@ -408,8 +424,7 @@ def _datasource_ex(self, resource, query=None, client_projection=None, # default sort is activated only if 'sorting' is enabled for the # resource. # TODO Consider raising a validation error on startup instead? - sort = sort_ if sort_ and config.DOMAIN[resource]['sorting'] else \ - None + sort = sort_ if sort_ and config.DOMAIN[resource]["sorting"] else None if filter_: if query: @@ -436,10 +451,11 @@ def _datasource_ex(self, resource, query=None, client_projection=None, if 1 in client_projection.values(): # inclusive projection - all values are 0 unless spec. or # auto - fields = dict([(field, field in keep_fields) for field in - fields.keys()]) + fields = dict( + [(field, field in keep_fields) for field in fields.keys()] + ) for field, value in client_projection.items(): - field_base = field.split('.')[0] + field_base = field.split(".")[0] if field_base not in keep_fields and field_base in fields: fields[field] = value else: @@ -448,16 +464,17 @@ def _datasource_ex(self, resource, query=None, client_projection=None, fields = client_projection # always drop exclusion projection, thus avoid mixed projection not # supported by db driver - fields = dict([(field, 1) for field, value in fields.items() if - value]) + fields = dict([(field, 1) for field, value in fields.items() if value]) # If the current HTTP method is in `public_methods` or # `public_item_methods`, skip the `auth_field` check # Only inject the auth_field in the query when not creating new # documents. - if request and request.method != 'POST' and ( - check_auth_value or force_auth_field_projection + if ( + request + and request.method != "POST" + and (check_auth_value or force_auth_field_projection) ): auth_field, request_auth_value = auth_field_and_value(resource) if auth_field: @@ -467,14 +484,15 @@ def _datasource_ex(self, resource, query=None, client_projection=None, # and the values are /different/, deny the request # This prevents the auth_field condition from # overwriting the query (issue #77) - auth_field_in_query = \ - self.app.data.query_contains_field(query, - auth_field) - if auth_field_in_query and \ - self.app.data.get_value_from_query( - query, auth_field) != request_auth_value: - desc = 'Incompatible User-Restricted Resource ' \ - 'request.' + auth_field_in_query = self.app.data.query_contains_field( + query, auth_field + ) + if ( + auth_field_in_query + and self.app.data.get_value_from_query(query, auth_field) + != request_auth_value + ): + desc = "Incompatible User-Restricted Resource " "request." abort(401, description=desc) else: query = self.app.data.combine_queries( @@ -501,10 +519,12 @@ def _client_projection(self, req): try: client_projection = json.loads(req.projection) if not isinstance(client_projection, dict): - raise Exception('The projection parameter has to be a ' - 'dict') + raise Exception("The projection parameter has to be a " "dict") except: - abort(400, description=debug_error_message( - 'Unable to parse `projection` clause' - )) + abort( + 400, + description=debug_error_message( + "Unable to parse `projection` clause" + ), + ) return client_projection diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index 6b4d077e9..6d44626f0 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -20,17 +20,17 @@ class PyMongo(object): Creates Mongo connection and database based on Flask configuration. """ - def __init__(self, app, config_prefix='MONGO'): - if 'pymongo' not in app.extensions: - app.extensions['pymongo'] = {} + def __init__(self, app, config_prefix="MONGO"): + if "pymongo" not in app.extensions: + app.extensions["pymongo"] = {} - if config_prefix in app.extensions['pymongo']: + if config_prefix in app.extensions["pymongo"]: raise Exception('duplicate config_prefix "%s"' % config_prefix) self.config_prefix = config_prefix def key(suffix): - return '%s_%s' % (config_prefix, suffix) + return "%s_%s" % (config_prefix, suffix) def config_to_kwargs(mapping): """ @@ -43,62 +43,57 @@ def config_to_kwargs(mapping): kwargs[arg] = app.config[key(option)] return kwargs - app.config.setdefault(key('HOST'), 'localhost') - app.config.setdefault(key('PORT'), 27017) - app.config.setdefault(key('DBNAME'), app.name) - app.config.setdefault(key('WRITE_CONCERN'), {'w': 1}) - client_kwargs = { - 'appname': app.name, - 'connect': True, - 'tz_aware': True, - } - if key('OPTIONS') in app.config: - client_kwargs.update(app.config[key('OPTIONS')]) - - if key('WRITE_CONCERN') in app.config: + app.config.setdefault(key("HOST"), "localhost") + app.config.setdefault(key("PORT"), 27017) + app.config.setdefault(key("DBNAME"), app.name) + app.config.setdefault(key("WRITE_CONCERN"), {"w": 1}) + client_kwargs = {"appname": app.name, "connect": True, "tz_aware": True} + if key("OPTIONS") in app.config: + client_kwargs.update(app.config[key("OPTIONS")]) + + if key("WRITE_CONCERN") in app.config: # w, wtimeout, j and fsync - client_kwargs.update(app.config[key('WRITE_CONCERN')]) + client_kwargs.update(app.config[key("WRITE_CONCERN")]) uri_parser.validate_options(client_kwargs) - if key('URI') in app.config: - host = app.config[key('URI')] + if key("URI") in app.config: + host = app.config[key("URI")] # raises an exception if uri is invalid mongo_settings = uri_parser.parse_uri(host) - dbname = mongo_settings.get('database') + dbname = mongo_settings.get("database") if not dbname: - dbname = app.config[key('DBNAME')] + dbname = app.config[key("DBNAME")] else: - dbname = app.config[key('DBNAME')] - host = app.config[key('HOST')] - client_kwargs['port'] = app.config[key('PORT')] + dbname = app.config[key("DBNAME")] + host = app.config[key("HOST")] + client_kwargs["port"] = app.config[key("PORT")] - client_kwargs['host'] = host + client_kwargs["host"] = host - if key('DOCUMENT_CLASS') in app.config: - client_kwargs['document_class'] = app.config[key('DOCUMENT_CLASS')] + if key("DOCUMENT_CLASS") in app.config: + client_kwargs["document_class"] = app.config[key("DOCUMENT_CLASS")] cx = MongoClient(**client_kwargs) db = cx[dbname] - if key('USERNAME') in app.config: - app.config.setdefault(key('PASSWORD'), None) - username = app.config[key('USERNAME')] - password = app.config[key('PASSWORD')] + if key("USERNAME") in app.config: + app.config.setdefault(key("PASSWORD"), None) + username = app.config[key("USERNAME")] + password = app.config[key("PASSWORD")] auth = (username, password) if any(auth) and not all(auth): - raise Exception( - 'Must set both USERNAME and PASSWORD or neither') + raise Exception("Must set both USERNAME and PASSWORD or neither") if any(auth): auth_mapping = { - 'AUTH_MECHANISM': 'mechanism', - 'AUTH_SOURCE': 'source', - 'AUTH_MECHANISM_PROPERTIES': 'authMechanismProperties', + "AUTH_MECHANISM": "mechanism", + "AUTH_SOURCE": "source", + "AUTH_MECHANISM_PROPERTIES": "authMechanismProperties", } auth_kwargs = config_to_kwargs(auth_mapping) db.authenticate(username, password, **auth_kwargs) - app.extensions['pymongo'][config_prefix] = (cx, db) + app.extensions["pymongo"][config_prefix] = (cx, db) @property def cx(self): @@ -106,9 +101,9 @@ def cx(self): Automatically created :class:`~pymongo.Connection` object corresponding to the provided configuration parameters. """ - if self.config_prefix not in current_app.extensions['pymongo']: - raise Exception('flask_pymongo extensions is not initialized') - return current_app.extensions['pymongo'][self.config_prefix][0] + if self.config_prefix not in current_app.extensions["pymongo"]: + raise Exception("flask_pymongo extensions is not initialized") + return current_app.extensions["pymongo"][self.config_prefix][0] @property def db(self): @@ -116,6 +111,6 @@ def db(self): Automatically created :class:`~pymongo.Database` object corresponding to the provided configuration parameters. """ - if self.config_prefix not in current_app.extensions['pymongo']: - raise Exception('flask_pymongo extensions is not initialized') - return current_app.extensions['pymongo'][self.config_prefix][1] + if self.config_prefix not in current_app.extensions["pymongo"]: + raise Exception("flask_pymongo extensions is not initialized") + return current_app.extensions["pymongo"][self.config_prefix][1] diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index a383b3fb9..8c6cee521 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -15,27 +15,29 @@ class GeoJSON(dict): def __init__(self, json): try: - self['type'] = json['type'] + self["type"] = json["type"] except KeyError: raise TypeError("Not compliant to GeoJSON") self.update(json) - if not config.ALLOW_CUSTOM_FIELDS_IN_GEOJSON and \ - len(self.keys()) != 2: + if not config.ALLOW_CUSTOM_FIELDS_IN_GEOJSON and len(self.keys()) != 2: raise TypeError("Not compliant to GeoJSON") def _correct_position(self, position): - return isinstance(position, list) and \ - len(position) > 1 and \ - all(isinstance(pos, int) or isinstance(pos, float) - for pos in position) + return ( + isinstance(position, list) + and len(position) > 1 + and all(isinstance(pos, int) or isinstance(pos, float) for pos in position) + ) class Geometry(GeoJSON): def __init__(self, json): super(Geometry, self).__init__(json) try: - if not isinstance(self['coordinates'], list) or \ - self['type'] != self.__class__.__name__: + if ( + not isinstance(self["coordinates"], list) + or self["type"] != self.__class__.__name__ + ): raise TypeError except (KeyError, TypeError): raise TypeError("Geometry not compliant to GeoJSON") @@ -45,9 +47,9 @@ class GeometryCollection(GeoJSON): def __init__(self, json): super(GeometryCollection, self).__init__(json) try: - if not isinstance(self['geometries'], list): + if not isinstance(self["geometries"], list): raise TypeError - for geometry in self['geometries']: + for geometry in self["geometries"]: factory = factories[geometry["type"]] factory(geometry) except (KeyError, TypeError, AttributeError): @@ -57,7 +59,7 @@ def __init__(self, json): class Point(Geometry): def __init__(self, json): super(Point, self).__init__(json) - if not self._correct_position(self['coordinates']): + if not self._correct_position(self["coordinates"]): raise TypeError @@ -129,7 +131,17 @@ def __init__(self, json): raise TypeError("FeatureCollection not compliant to GeoJSON") -factories = dict([(_type.__name__, _type) - for _type in - [GeometryCollection, Point, MultiPoint, LineString, - MultiLineString, Polygon, MultiPolygon]]) +factories = dict( + [ + (_type.__name__, _type) + for _type in [ + GeometryCollection, + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + ] + ] +) diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 95e9420c8..898c11904 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -41,10 +41,10 @@ def validate(self): instance. """ if self.app is None: - raise TypeError('Application object cannot be None') + raise TypeError("Application object cannot be None") if not isinstance(self.app, Flask): - raise TypeError('Application object must be a Eve application') + raise TypeError("Application object must be a Eve application") def fs(self, resource=None): """ Provides the instance-level GridFS instance, instantiating it if @@ -55,8 +55,7 @@ def fs(self, resource=None): """ driver = self.app.data if driver is None or not isinstance(driver, Mongo): - raise TypeError("Application data object must be of eve.io.Mongo " - "type.") + raise TypeError("Application data object must be of eve.io.Mongo " "type.") px = driver.current_mongo_prefix(resource) if px not in self._fs: @@ -89,8 +88,9 @@ def put(self, content, filename=None, content_type=None, resource=None): """ Saves a new file in GridFS. Returns the unique id of the stored file. Also stores content type of the file. """ - return self.fs(resource).put(content, filename=filename, - content_type=content_type) + return self.fs(resource).put( + content, filename=filename, content_type=content_type + ) def delete(self, _id, resource=None): """ Deletes the file referenced by unique id. diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index da2a14928..4ed3dec39 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -28,8 +28,13 @@ from eve.auth import resource_auth from eve.io.base import DataLayer, ConnectionException, BaseJSONEncoder from eve.io.mongo.parser import parse, ParseError -from eve.utils import config, debug_error_message, validate_filters, \ - str_to_date, str_type +from eve.utils import ( + config, + debug_error_message, + validate_filters, + str_to_date, + str_type, +) class MongoJSONEncoder(BaseJSONEncoder): @@ -41,6 +46,7 @@ class MongoJSONEncoder(BaseJSONEncoder): .. versionadded:: 0.2 """ + def default(self, obj): if isinstance(obj, ObjectId): # BSON/Mongo ObjectId is rendered as a string @@ -51,9 +57,9 @@ def default(self, obj): # (and we probably don't want it to be exposed anyway). See #790. return "" if isinstance(obj, DBRef): - retval = {'$id': str(obj.id), '$ref': obj.collection} + retval = {"$id": str(obj.id), "$ref": obj.collection} if obj.database: - retval['$db'] = obj.database + retval["$db"] = obj.database return retval if isinstance(obj, decimal128.Decimal128): return str(obj) @@ -79,19 +85,22 @@ class Mongo(DataLayer): """ serializers = { - 'objectid': lambda value: ObjectId(value) if value else None, - 'datetime': str_to_date, - 'integer': lambda value: int(value) if value is not None else None, - 'float': lambda value: float(value) if value is not None else None, - 'number': lambda val: json.loads(val) if val is not None else None, - 'boolean': lambda v: - {'1': True, 'true': True, '0': False, 'false': False}[str(v).lower()], - 'dbref': lambda value: - DBRef(value['$col'], value['$id'], value['$db'] - if '$db' in value else None) if value is not None else None, - 'decimal': lambda value: - decimal128.Decimal128(decimal.Decimal(str(value))) - if value is not None else None, + "objectid": lambda value: ObjectId(value) if value else None, + "datetime": str_to_date, + "integer": lambda value: int(value) if value is not None else None, + "float": lambda value: float(value) if value is not None else None, + "number": lambda val: json.loads(val) if val is not None else None, + "boolean": lambda v: {"1": True, "true": True, "0": False, "false": False}[ + str(v).lower() + ], + "dbref": lambda value: DBRef( + value["$col"], value["$id"], value["$db"] if "$db" in value else None + ) + if value is not None + else None, + "decimal": lambda value: decimal128.Decimal128(decimal.Decimal(str(value))) + if value is not None + else None, } # JSON serializer is a class attribute. Allows extensions to replace it @@ -99,15 +108,15 @@ class Mongo(DataLayer): json_encoder_class = MongoJSONEncoder operators = set( - ['$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin'] + - ['$or', '$and', '$not', '$nor'] + - ['$mod', '$regex', '$text', '$where'] + - ['$options', '$search', '$language', '$caseSensitive'] + - ['$diacriticSensitive', '$exists', '$type'] + - ['$geoWithin', '$geoIntersects', '$near', '$nearSphere'] + - ['$geometry', '$maxDistance', '$box'] + - ['$all', '$elemMatch', '$size'] + - ['$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'] + ["$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin"] + + ["$or", "$and", "$not", "$nor"] + + ["$mod", "$regex", "$text", "$where"] + + ["$options", "$search", "$language", "$caseSensitive"] + + ["$diacriticSensitive", "$exists", "$type"] + + ["$geoWithin", "$geoIntersects", "$near", "$nearSphere"] + + ["$geometry", "$maxDistance", "$box"] + + ["$all", "$elemMatch", "$size"] + + ["$bitsAllClear", "$bitsAllSet", "$bitsAnyClear", "$bitsAnySet"] ) def init_app(self, app): @@ -188,10 +197,10 @@ def find(self, resource, req, sub_resource_lookup): args = dict() if req and req.max_results: - args['limit'] = req.max_results + args["limit"] = req.max_results if req and req.page > 1: - args['skip'] = (req.page - 1) * req.max_results + args["skip"] = (req.page - 1) * req.max_results # TODO sort syntax should probably be coherent with 'where': either # mongo-like # or python-like. Currently accepts only mongo-like sort @@ -233,9 +242,12 @@ def find(self, resource, req, sub_resource_lookup): try: spec = parse(req.where) except ParseError: - abort(400, description=debug_error_message( - 'Unable to parse `where` clause' - )) + abort( + 400, + description=debug_error_message( + "Unable to parse `where` clause" + ), + ) bad_filter = validate_filters(spec, resource) if bad_filter: @@ -244,9 +256,11 @@ def find(self, resource, req, sub_resource_lookup): if sub_resource_lookup: spec = self.combine_queries(spec, sub_resource_lookup) - if config.DOMAIN[resource]['soft_delete'] \ - and not (req and req.show_deleted) \ - and not self.query_contains_field(spec, config.DELETED): + if ( + config.DOMAIN[resource]["soft_delete"] + and not (req and req.show_deleted) + and not self.query_contains_field(spec, config.DELETED) + ): # Soft delete filtering applied after validate_filters call as # querying against the DELETED field must always be allowed when # soft_delete is enabled @@ -257,28 +271,31 @@ def find(self, resource, req, sub_resource_lookup): client_projection = self._client_projection(req) datasource, spec, projection, sort = self._datasource_ex( - resource, - spec, - client_projection, - client_sort) + resource, spec, client_projection, client_sort + ) if req and req.if_modified_since: - spec[config.LAST_UPDATED] = \ - {'$gt': req.if_modified_since} + spec[config.LAST_UPDATED] = {"$gt": req.if_modified_since} if len(spec) > 0: - args['filter'] = spec + args["filter"] = spec if sort is not None: - args['sort'] = sort + args["sort"] = sort if projection: - args['projection'] = projection + args["projection"] = projection return self.pymongo(resource).db[datasource].find(**args) - def find_one(self, resource, req, check_auth_value=True, - force_auth_field_projection=False, **lookup): + def find_one( + self, + resource, + req, + check_auth_value=True, + force_auth_field_projection=False, + **lookup + ): """ Retrieves a single document. :param resource: resource name. @@ -315,16 +332,19 @@ def find_one(self, resource, req, check_auth_value=True, lookup, client_projection, check_auth_value=check_auth_value, - force_auth_field_projection=force_auth_field_projection) + force_auth_field_projection=force_auth_field_projection, + ) - if (config.DOMAIN[resource]['soft_delete']) and \ - (not req or not req.show_deleted) and \ - (not self.query_contains_field(lookup, config.DELETED)): - filter_ = self.combine_queries( - filter_, {config.DELETED: {"$ne": True}}) + if ( + (config.DOMAIN[resource]["soft_delete"]) + and (not req or not req.show_deleted) + and (not self.query_contains_field(lookup, config.DELETED)) + ): + filter_ = self.combine_queries(filter_, {config.DELETED: {"$ne": True}}) # Here, we feed pymongo with `None` if projection is empty. - return self.pymongo(resource).db[datasource] \ - .find_one(filter_, projection or None) + return ( + self.pymongo(resource).db[datasource].find_one(filter_, projection or None) + ) def find_one_raw(self, resource, **lookup): """ Retrieves a single raw document. @@ -337,11 +357,9 @@ def find_one_raw(self, resource, **lookup): .. versionadded:: 0.4 """ - id_field = config.DOMAIN[resource]['id_field'] + id_field = config.DOMAIN[resource]["id_field"] _id = lookup.get(id_field) - datasource, filter_, _, _ = self._datasource_ex(resource, - {id_field: _id}, - None) + datasource, filter_, _, _ = self._datasource_ex(resource, {id_field: _id}, None) lookup = self._mongotize(lookup, resource) @@ -382,10 +400,8 @@ def find_list_of_ids(self, resource, ids, client_projection=None): .. versionadded:: 0.1.0 """ - id_field = config.DOMAIN[resource]['id_field'] - query = {'$or': [ - {id_field: id_} for id_ in ids - ]} + id_field = config.DOMAIN[resource]["id_field"] + query = {"$or": [{id_field: id_} for id_ in ids]} datasource, spec, projection, _ = self._datasource_ex( resource, query=query, client_projection=client_projection @@ -393,8 +409,10 @@ def find_list_of_ids(self, resource, ids, client_projection=None): # projection of {} return all fields in MongoDB, but # pymongo will only return `_id`. It's a design flaw upstream. # Here, we feed pymongo with `None` if projection is empty. - documents = self.pymongo(resource).db[datasource].find( - filter=spec, projection=(projection or None) + documents = ( + self.pymongo(resource) + .db[datasource] + .find(filter=spec, projection=(projection or None)) ) return documents @@ -403,11 +421,9 @@ def aggregate(self, resource, pipeline, options): .. versionadded:: 0.7 """ datasource, _, _, _ = self.datasource(resource) - challenge = self._mongotize({'key': pipeline}, resource)['key'] + challenge = self._mongotize({"key": pipeline}, resource)["key"] - return self.pymongo(resource).db[datasource].aggregate( - challenge, **options - ) + return self.pymongo(resource).db[datasource].aggregate(challenge, **options) def insert(self, resource, doc_or_docs): """ Inserts a document into a resource collection. @@ -452,18 +468,24 @@ def insert(self, resource, doc_or_docs): # report a duplicate key error since this can probably be # handled by the client. - for error in e.details['writeErrors']: + for error in e.details["writeErrors"]: # amazingly enough, pymongo does not appear to be exposing # error codes as constants. - if error['code'] == 11000: - abort(409, description=debug_error_message( - 'Duplicate key error at index: %s, message: %s' % ( - error['index'], error['errmsg']) - )) - - abort(500, description=debug_error_message( - 'pymongo.errors.BulkWriteError: %s' % e - )) + if error["code"] == 11000: + abort( + 409, + description=debug_error_message( + "Duplicate key error at index: %s, message: %s" + % (error["index"], error["errmsg"]) + ), + ) + + abort( + 500, + description=debug_error_message( + "pymongo.errors.BulkWriteError: %s" % e + ), + ) def _change_request(self, resource, id_, changes, original, replace=False): """ Performs a change, be it a replace or update. @@ -475,48 +497,52 @@ def _change_request(self, resource, id_, changes, original, replace=False): Return 400 if an attempt is made to update/replace an immutable field. """ - id_field = config.DOMAIN[resource]['id_field'] + id_field = config.DOMAIN[resource]["id_field"] query = {id_field: id_} if config.ETAG in original: query[config.ETAG] = original[config.ETAG] - datasource, filter_, _, _ = self._datasource_ex( - resource, query) + datasource, filter_, _, _ = self._datasource_ex(resource, query) coll = self.get_collection_with_write_concern(datasource, resource) try: - coll.replace_one(filter_, changes) if replace else \ - coll.update_one(filter_, changes) + coll.replace_one(filter_, changes) if replace else coll.update_one( + filter_, changes + ) except pymongo.errors.DuplicateKeyError as e: - abort(400, description=debug_error_message( - 'pymongo.errors.DuplicateKeyError: %s' % e - )) + abort( + 400, + description=debug_error_message( + "pymongo.errors.DuplicateKeyError: %s" % e + ), + ) except pymongo.errors.OperationFailure as e: # server error codes and messages changed between 2.4 and 2.6/3.0. - server_version = \ - self.driver.db.client.server_info()['version'][:3] - if ( - (server_version == '2.4' and e.code in (13596, 10148)) or - (server_version in ('2.6', '3.0', '3.2', '3.4') and - e.code in (66, 16837)) + server_version = self.driver.db.client.server_info()["version"][:3] + if (server_version == "2.4" and e.code in (13596, 10148)) or ( + server_version in ("2.6", "3.0", "3.2", "3.4") and e.code in (66, 16837) ): # attempt to update an immutable field. this usually # happens when a PATCH or PUT includes a mismatching ID_FIELD. self.app.logger.warning(e) - description = debug_error_message( - 'pymongo.errors.OperationFailure: %s' % e) or \ - "Attempt to update an immutable field. Usually happens " \ - "when PATCH or PUT include a '%s' field, " \ - "which is immutable (PUT can include it as long as " \ + description = ( + debug_error_message("pymongo.errors.OperationFailure: %s" % e) + or "Attempt to update an immutable field. Usually happens " + "when PATCH or PUT include a '%s' field, " + "which is immutable (PUT can include it as long as " "it is unchanged)." % id_field + ) abort(400, description=description) else: # see comment in :func:`insert()`. self.app.logger.exception(e) - abort(500, description=debug_error_message( - 'pymongo.errors.OperationFailure: %s' % e - )) + abort( + 500, + description=debug_error_message( + "pymongo.errors.OperationFailure: %s" % e + ), + ) def update(self, resource, id_, updates, original): """ Updates a collection document. @@ -571,8 +597,7 @@ def replace(self, resource, id_, document, original): .. versionadded:: 0.1.0 """ - return self._change_request(resource, id_, document, original, - replace=True) + return self._change_request(resource, id_, document, original, replace=True) def remove(self, resource, lookup): """ Removes a document or the entire set of documents from a @@ -619,9 +644,12 @@ def remove(self, resource, lookup): except pymongo.errors.OperationFailure as e: # see comment in :func:`insert()`. self.app.logger.exception(e) - abort(500, description=debug_error_message( - 'pymongo.errors.OperationFailure: %s' % e - )) + abort( + 500, + description=debug_error_message( + "pymongo.errors.OperationFailure: %s" % e + ), + ) # TODO: The next three methods could be pulled out to form the basis # of a separate MonqoQuery class @@ -658,9 +686,8 @@ def combine_queries(self, query_a, query_b): """ # Chain the operations with the $and operator return { - '$and': [ - {k: v} for k, v in itertools.chain(query_a.items(), - query_b.items()) + "$and": [ + {k: v} for k, v in itertools.chain(query_a.items(), query_b.items()) ] } @@ -684,8 +711,8 @@ def get_value_from_query(self, query, field_name): """ if field_name in query: return query[field_name] - elif '$and' in query: - for condition in query['$and']: + elif "$and" in query: + for condition in query["$and"]: if field_name in condition: return condition[field_name] raise KeyError @@ -733,9 +760,12 @@ def is_empty(self, resource): except pymongo.errors.OperationFailure as e: # see comment in :func:`insert()`. self.app.logger.exception(e) - abort(500, description=debug_error_message( - 'pymongo.errors.OperationFailure: %s' % e - )) + abort( + 500, + description=debug_error_message( + "pymongo.errors.OperationFailure: %s" % e + ), + ) def _mongotize(self, source, resource): """ Recursively iterates a JSON dictionary, turning RFC-1123 strings @@ -758,7 +788,7 @@ def _mongotize(self, source, resource): .. versionadded:: 0.0.4 """ schema = config.DOMAIN[resource] - skip_objectid = schema.get('query_objectid_as_string', False) + skip_objectid = schema.get("query_objectid_as_string", False) def try_cast(v): try: @@ -809,19 +839,26 @@ def _sanitize(self, spec): .. versionadded:: 0.0.7 """ + def sanitize_keys(spec): - ops = set([op for op in spec.keys() if op[0] == '$']) + ops = set([op for op in spec.keys() if op[0] == "$"]) unknown = ops - Mongo.operators if unknown: - abort(400, description=debug_error_message( - 'Query contains unknown or unsupported operators: %s' % - ', '.join(unknown) - )) + abort( + 400, + description=debug_error_message( + "Query contains unknown or unsupported operators: %s" + % ", ".join(unknown) + ), + ) if set(spec.keys()) & set(config.MONGO_QUERY_BLACKLIST): - abort(400, description=debug_error_message( - 'Query contains operators banned in MONGO_QUERY_BLACKLIST' - )) + abort( + 400, + description=debug_error_message( + "Query contains operators banned in MONGO_QUERY_BLACKLIST" + ), + ) if isinstance(spec, dict): sanitize_keys(spec) @@ -838,7 +875,7 @@ def _wc(self, resource): .. versionadded:: 0.0.8 """ - return config.DOMAIN[resource]['mongo_write_concern'] + return config.DOMAIN[resource]["mongo_write_concern"] def current_mongo_prefix(self, resource=None): """ Returns the active mongo_prefix that should be used to retrieve @@ -870,7 +907,7 @@ def current_mongo_prefix(self, resource=None): auth = None try: if resource is None and request and request.endpoint: - resource = request.endpoint[:request.endpoint.index('|')] + resource = request.endpoint[: request.endpoint.index("|")] if request and request.endpoint: auth = resource_auth(resource) except ValueError: @@ -879,13 +916,13 @@ def current_mongo_prefix(self, resource=None): px = auth.get_mongo_prefix() if auth else None if px is None: - px = g.get('mongo_prefix', None) + px = g.get("mongo_prefix", None) if px is None: if resource: - px = config.DOMAIN[resource].get('mongo_prefix', 'MONGO') + px = config.DOMAIN[resource].get("mongo_prefix", "MONGO") else: - px = 'MONGO' + px = "MONGO" return px @@ -924,9 +961,8 @@ def get_collection_with_write_concern(self, datasource, resource): .. versionadded:: 0.6.1 """ - wc = WriteConcern(config.DOMAIN[resource]['mongo_write_concern']['w']) - return self.pymongo(resource).db[datasource].with_options( - write_concern=wc) + wc = WriteConcern(config.DOMAIN[resource]["mongo_write_concern"]["w"]) + return self.pymongo(resource).db[datasource].with_options(write_concern=wc) class PyMongos(dict): @@ -935,6 +971,7 @@ class PyMongos(dict): .. versionadded:: 0.6 """ + def __init__(self, mongo, *args): self.mongo = mongo dict.__init__(self, args) @@ -955,7 +992,7 @@ def ensure_mongo_indexes(app, resource): .. versionaddded:: 0.8 """ - mongo_indexes = app.config['DOMAIN'][resource]['mongo_indexes'] + mongo_indexes = app.config["DOMAIN"][resource]["mongo_indexes"] if not mongo_indexes: return @@ -996,24 +1033,24 @@ def _create_index(app, resource, name, list_of_keys, index_options): # it doesn't work as a typical mongodb method run in the request # life cycle, it is just called when the app start and it uses # pymongo directly. - collection = app.config['SOURCES'][resource]['source'] + collection = app.config["SOURCES"][resource]["source"] # get db for given prefix try: # mongo_prefix might have been set by Auth class instance - px = g.get('mongo_prefix') + px = g.get("mongo_prefix") except: - px = app.config['DOMAIN'][resource].get('mongo_prefix', 'MONGO') + px = app.config["DOMAIN"][resource].get("mongo_prefix", "MONGO") with app.app_context(): db = app.data.pymongo(resource, px).db kw = copy(index_options) - kw['name'] = name + kw["name"] = name colls = [db[collection]] - if app.config['DOMAIN'][resource]['versioning']: - colls.append(db['%s_versions' % collection]) + if app.config["DOMAIN"][resource]["versioning"]: + colls.append(db["%s_versions" % collection]) for coll in colls: try: diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index ac633c098..af0d811ef 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -13,8 +13,8 @@ import ast import sys -from datetime import datetime # noqa -from bson import ObjectId # noqa +from datetime import datetime # noqa +from bson import ObjectId # noqa def parse(expression): @@ -45,15 +45,16 @@ class MongoVisitor(ast.NodeVisitor): Supported compare operators: ==, >, <, !=, >=, <= Supported boolean operators: And, Or """ + op_mapper = { - ast.Eq: '', - ast.Gt: '$gt', - ast.GtE: '$gte', - ast.Lt: '$lt', - ast.LtE: '$lte', - ast.NotEq: '$ne', - ast.Or: '$or', - ast.And: '$and' + ast.Eq: "", + ast.Gt: "$gt", + ast.GtE: "$gte", + ast.Lt: "$lt", + ast.LtE: "$lte", + ast.NotEq: "$ne", + ast.Or: "$or", + ast.And: "$and", } def visit_Module(self, node): @@ -69,15 +70,18 @@ def visit_Module(self, node): # if we didn't obtain a query, it is likely that an unsupported # python expression has been passed. if self.mongo_query == {}: - raise ParseError("Only conditional statements with boolean " - "(and, or) and comparison operators are " - "supported.") + raise ParseError( + "Only conditional statements with boolean " + "(and, or) and comparison operators are " + "supported." + ) def visit_Expr(self, node): """ Make sure that we are parsing compare or boolean operators """ - if not (isinstance(node.value, ast.Compare) or - isinstance(node.value, ast.BoolOp)): + if not ( + isinstance(node.value, ast.Compare) or isinstance(node.value, ast.BoolOp) + ): raise ParseError("Will only parse conditional statements") self.generic_visit(node) @@ -93,7 +97,7 @@ def visit_Compare(self, node): comparator = node.comparators[0] self.visit(comparator) - if operator != '': + if operator != "": value = {operator: self.current_value} else: value = self.current_value @@ -122,12 +126,12 @@ def visit_Call(self, node): datetime(). """ if isinstance(node.func, ast.Name): - if node.func.id == 'ObjectId': + if node.func.id == "ObjectId": try: self.current_value = ObjectId(node.args[0].s) except: pass - elif node.func.id == 'datetime': + elif node.func.id == "datetime": values = [] for arg in node.args: values.append(arg.n) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 7fc59c9bf..6f798b8ec 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -17,9 +17,17 @@ from werkzeug.datastructures import FileStorage from eve.auth import auth_field_and_value -from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \ - MultiLineString, MultiPolygon, GeometryCollection, Feature, \ - FeatureCollection +from eve.io.mongo.geo import ( + Point, + MultiPoint, + LineString, + Polygon, + MultiLineString, + MultiPolygon, + GeometryCollection, + Feature, + FeatureCollection, +) from eve.utils import config from eve.validation import Validator from eve.versioning import get_data_version_relation_document @@ -51,6 +59,7 @@ class Validator(Validator): Support for 'transparent_schema_rules' introduced with Cerberus 0.0.3, which allows for insertion of 'default' values in POST requests. """ + def _validate_versioned(self, unique, field, value): """ {'type': 'boolean'} """ pass @@ -83,7 +92,7 @@ def _is_value_unique(self, unique, field, value, query): resource_config = config.DOMAIN[self.resource] # exclude soft deleted documents if applicable - if resource_config['soft_delete']: + if resource_config["soft_delete"]: # be aware that, should a previously (soft) deleted document be # restored, and because we explicitly ignore soft deleted # documents while validating 'unique' fields, there is a chance @@ -95,12 +104,12 @@ def _is_value_unique(self, unique, field, value, query): # we make sure to also include documents which are missing the # DELETED field. This happens when soft deletes are enabled on # an a resource with existing documents. - query[config.DELETED] = {'$ne': True} + query[config.DELETED] = {"$ne": True} # exclude current document if self.document_id: - id_field = resource_config['id_field'] - query[id_field] = {'$ne': self.document_id} + id_field = resource_config["id_field"] + query[id_field] = {"$ne": self.document_id} # we perform the check on the native mongo driver (and not on # app.data.find_one()) because in this case we don't want the usual @@ -119,49 +128,65 @@ def _validate_data_relation(self, data_relation, field, value): 'embeddable': {'type': 'boolean', 'default': False}, 'version': {'type': 'boolean', 'default': False} }} """ - if 'version' in data_relation and data_relation['version'] is True: - value_field = data_relation['field'] - version_field = app.config['VERSION'] + if "version" in data_relation and data_relation["version"] is True: + value_field = data_relation["field"] + version_field = app.config["VERSION"] # check value format - if isinstance(value, dict) and value_field in value \ - and version_field in value: - resource_def = config.DOMAIN[data_relation['resource']] - if resource_def['versioning'] is False: + if ( + isinstance(value, dict) + and value_field in value + and version_field in value + ): + resource_def = config.DOMAIN[data_relation["resource"]] + if resource_def["versioning"] is False: self._error( - field, "can't save a version with" - " data_relation if '%s' isn't versioned" % - data_relation['resource']) + field, + "can't save a version with" + " data_relation if '%s' isn't versioned" + % data_relation["resource"], + ) else: - search = get_data_version_relation_document( - data_relation, value) + search = get_data_version_relation_document(data_relation, value) if not search: self._error( - field, "value '%s' must exist in resource" - " '%s', field '%s' at version '%s'." % ( - value[value_field], data_relation['resource'], - data_relation['field'], value[version_field])) + field, + "value '%s' must exist in resource" + " '%s', field '%s' at version '%s'." + % ( + value[value_field], + data_relation["resource"], + data_relation["field"], + value[version_field], + ), + ) else: self._error( - field, "versioned data_relation must be a dict" - " with fields '%s' and '%s'" % - (value_field, version_field)) + field, + "versioned data_relation must be a dict" + " with fields '%s' and '%s'" % (value_field, version_field), + ) else: if not isinstance(value, list): value = [value] - data_resource = data_relation['resource'] + data_resource = data_relation["resource"] for item in value: - query = {data_relation['field']: item.id - if isinstance(item, DBRef) else item} - if not app.data.find_one(data_resource, None, **query): - self._error( - field, - "value '%s' must exist in resource" - " '%s', field '%s'." % - (item.id if isinstance(item, DBRef) else item, - data_resource, data_relation['field'])) + query = { + data_relation["field"]: item.id if isinstance(item, DBRef) else item + } + if not app.data.find_one(data_resource, None, **query): + self._error( + field, + "value '%s' must exist in resource" + " '%s', field '%s'." + % ( + item.id if isinstance(item, DBRef) else item, + data_resource, + data_relation["field"], + ), + ) def _validate_type_objectid(self, value): if ObjectId.is_valid(value): diff --git a/eve/logging.py b/eve/logging.py index 6c9575771..609b1cc0d 100644 --- a/eve/logging.py +++ b/eve/logging.py @@ -8,6 +8,7 @@ # add support for some INFO and maybe DEBUG level logging (like, log each time # a endpoint is hit, etc.) + class RequestFilter(logging.Filter): """ Adds Flask's request metadata to the log record so handlers can log this information too. @@ -33,6 +34,7 @@ def log_a_get(resource, request, payload): .. versionadded:: 0.6 """ + def filter(self, record): if request: record.clientip = request.remote_addr diff --git a/eve/methods/common.py b/eve/methods/common.py index d80919b80..ac8841a01 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -22,16 +22,25 @@ from flask import Response, abort, current_app as app, g, request from werkzeug.datastructures import MultiDict, CombinedMultiDict -from eve.utils import auto_fields, config, debug_error_message, \ - document_etag, parse_request -from eve.versioning import get_data_version_relation_document, \ - resolve_document_version +from eve.utils import ( + auto_fields, + config, + debug_error_message, + document_etag, + parse_request, +) +from eve.versioning import get_data_version_relation_document, resolve_document_version from collections import Counter -def get_document(resource, concurrency_check, original=None, - check_auth_value=True, force_auth_field_projection=False, - **lookup): +def get_document( + resource, + concurrency_check, + original=None, + check_auth_value=True, + force_auth_field_projection=False, + **lookup +): """ Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client request references the current representation of the document before @@ -67,7 +76,7 @@ def get_document(resource, concurrency_check, original=None, processing of new configuration settings: `filters`, `sorting`, `paging`. """ req = parse_request(resource) - if config.DOMAIN[resource]['soft_delete']: + if config.DOMAIN[resource]["soft_delete"]: # get_document should always fetch soft deleted documents from the db # callers must handle soft deleted documents req.show_deleted = True @@ -75,9 +84,9 @@ def get_document(resource, concurrency_check, original=None, if original: document = original else: - document = app.data.find_one(resource, req, check_auth_value, - force_auth_field_projection, - **lookup) + document = app.data.find_one( + resource, req, check_auth_value, force_auth_field_projection, **lookup + ) if document: e_if_m = config.ENFORCE_IF_MATCH @@ -87,8 +96,11 @@ def get_document(resource, concurrency_check, original=None, # for the document or explicitly decides to allow editing by either # disabling the ``concurrency_check`` or ``IF_MATCH`` or # ``ENFORCE_IF_MATCH`` fields. - abort(428, description='To edit a document ' - 'its etag must be provided using the If-Match header') + abort( + 428, + description="To edit a document " + "its etag must be provided using the If-Match header", + ) # ensure the retrieved document has LAST_UPDATED and DATE_CREATED, # eventually with same default values as in GET. @@ -96,13 +108,14 @@ def get_document(resource, concurrency_check, original=None, document[config.DATE_CREATED] = date_created(document) if req.if_match and concurrency_check: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] - etag = document.get(config.ETAG, document_etag(document, - ignore_fields=ignore_fields)) + ignore_fields = config.DOMAIN[resource]["etag_ignore_fields"] + etag = document.get( + config.ETAG, document_etag(document, ignore_fields=ignore_fields) + ) if req.if_match != etag: # client and server etags must match, or we don't allow editing # (ensures that client's version of the document is up to date) - abort(412, description='Client and server etags don\'t match') + abort(412, description="Client and server etags don't match") return document @@ -172,14 +185,17 @@ def payload(): .. versionadded: 0.0.5 """ - content_type = request.headers.get('Content-Type', '').split(';')[0] + content_type = request.headers.get("Content-Type", "").split(";")[0] if content_type in config.JSON_REQUEST_CONTENT_TYPES: return request.get_json(force=True) - elif content_type == 'application/x-www-form-urlencoded': - return multidict_to_dict(request.form) if len(request.form) else \ - abort(400, description='No form-urlencoded data supplied') - elif content_type == 'multipart/form-data': + elif content_type == "application/x-www-form-urlencoded": + return ( + multidict_to_dict(request.form) + if len(request.form) + else abort(400, description="No form-urlencoded data supplied") + ) + elif content_type == "multipart/form-data": # as multipart is also used for file uploads, we let an empty # request.form go through as long as there are also files in the # request. @@ -203,9 +219,9 @@ def payload(): return multidict_to_dict(payload) else: - abort(400, description='No multipart/form-data supplied') + abort(400, description="No multipart/form-data supplied") else: - abort(400, description='Unknown or no Content-Type header supplied') + abort(400, description="Unknown or no Content-Type header supplied") def multidict_to_dict(multidict): @@ -234,6 +250,7 @@ class RateLimit(object): .. versionadded:: 0.0.7 """ + # Maybe has something complicated problems. def __init__(self, key, limit, period, send_x_headers=True): @@ -257,7 +274,7 @@ def get_rate_limit(): .. versionadded:: 0.0.7 """ - return getattr(g, '_rate_limit', None) + return getattr(g, "_rate_limit", None) def ratelimit(): @@ -274,28 +291,33 @@ def ratelimit(): .. versionadded:: 0.0.7 """ + def decorator(f): @wraps(f) def rate_limited(*args, **kwargs): - method_limit = app.config.get('RATE_LIMIT_' + request.method) + method_limit = app.config.get("RATE_LIMIT_" + request.method) if method_limit and app.redis: limit = method_limit[0] period = method_limit[1] # If authorization is being used the key is 'username'. # Else, fallback to client IP. - key = 'rate-limit/%s' % (request.authorization.username - if request.authorization else - request.remote_addr) + key = "rate-limit/%s" % ( + request.authorization.username + if request.authorization + else request.remote_addr + ) rlimit = RateLimit(key, limit, period, True) if rlimit.over_limit: - return Response('Rate limit exceeded', 429) + return Response("Rate limit exceeded", 429) # store the rate limit for further processing by # send_response g._rate_limit = rlimit else: g._rate_limit = None return f(*args, **kwargs) + return rate_limited + return decorator @@ -336,8 +358,7 @@ def date_created(document): .. versionadded:: 0.0.5 """ - return document[config.DATE_CREATED] if config.DATE_CREATED in document \ - else epoch() + return document[config.DATE_CREATED] if config.DATE_CREATED in document else epoch() def epoch(): @@ -379,14 +400,13 @@ def serialize(document, resource=None, schema=None, fields=None): """ def resolve_schema(schema): - return schema if isinstance(schema, dict) else \ - schema_registry.get(schema) + return schema if isinstance(schema, dict) else schema_registry.get(schema) normalize_dotted_fields(document) if app.data.serializers: if resource: - schema = resolve_schema(config.DOMAIN[resource]['schema']) + schema = resolve_schema(config.DOMAIN[resource]["schema"]) if not fields: fields = document.keys() for field in fields: @@ -396,107 +416,116 @@ def resolve_schema(schema): field_schema = schema[field] if not isinstance(field_schema, dict): field_schema = rules_set_registry.get(field_schema) - field_types = field_schema.get('type') + field_types = field_schema.get("type") if not isinstance(field_types, list): field_types = [field_types] for field_type in field_types: - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: for optschema in field_schema.get(x_of, []): optschema = dict(field_schema, **optschema) optschema.pop(x_of, None) serialize(document, schema={field: optschema}) - x_of_type = '{0}_type'.format(x_of) + x_of_type = "{0}_type".format(x_of) for opttype in field_schema.get(x_of_type, []): optschema = dict(field_schema, type=opttype) optschema.pop(x_of_type, None) serialize(document, schema={field: optschema}) - if config.AUTO_CREATE_LISTS and field_type == 'list': + if config.AUTO_CREATE_LISTS and field_type == "list": # Convert single values to lists if not isinstance(document[field], list): document[field] = [document[field]] - if 'schema' in field_schema: - field_schema = resolve_schema(field_schema['schema']) - if 'dict' in (field_type, field_schema.get('type')): + if "schema" in field_schema: + field_schema = resolve_schema(field_schema["schema"]) + if "dict" in (field_type, field_schema.get("type")): # either a dict or a list of dicts - embedded = [document[field]] \ - if field_type == 'dict' else document[field] + embedded = ( + [document[field]] + if field_type == "dict" + else document[field] + ) for subdocument in embedded: if type(subdocument) is not dict: # value is not a dict - continue # serialization error will be reported by # validation if appropriate continue - elif 'schema' in field_schema: - serialize(subdocument, - schema=field_schema['schema']) + elif "schema" in field_schema: + serialize( + subdocument, schema=field_schema["schema"] + ) else: serialize(subdocument, schema=field_schema) - elif field_schema.get('type') == 'list': + elif field_schema.get("type") == "list": # a list of lists - sublist_schema = resolve_schema( - field_schema.get('schema')) - item_type = sublist_schema.get('type') + sublist_schema = resolve_schema(field_schema.get("schema")) + item_type = sublist_schema.get("type") for sublist in document[field]: for i, v in enumerate(sublist): - if item_type == 'dict': + if item_type == "dict": serialize( - sublist[i], - schema=sublist_schema['schema']) + sublist[i], schema=sublist_schema["schema"] + ) elif item_type in app.data.serializers: - sublist[i] = serialize_value( - item_type, v) - elif field_schema.get('type') is None: + sublist[i] = serialize_value(item_type, v) + elif field_schema.get("type") is None: # a list of items determined by *of rules - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: for optschema in field_schema.get(x_of, []): - serialize(document, - schema={ - field: { - 'type': field_type, - 'schema': optschema}}) - x_of_type = '{0}_type'.format(x_of) - for opttype in field_schema.get( - x_of_type, []): serialize( document, - schema={field: {'type': field_type, - 'schema': {'type': - opttype}}}) + schema={ + field: { + "type": field_type, + "schema": optschema, + } + }, + ) + x_of_type = "{0}_type".format(x_of) + for opttype in field_schema.get(x_of_type, []): + serialize( + document, + schema={ + field: { + "type": field_type, + "schema": {"type": opttype}, + } + }, + ) else: # a list of one type, arbitrary length - field_type = field_schema.get('type') + field_type = field_schema.get("type") if field_type in app.data.serializers: for i, v in enumerate(document[field]): - document[field][i] = \ - serialize_value(field_type, v) - elif 'items' in field_schema: + document[field][i] = serialize_value(field_type, v) + elif "items" in field_schema: # a list of multiple types, fixed length - for i, (s, v) in enumerate(zip(field_schema['items'], - document[field])): - field_type = s.get('type') + for i, (s, v) in enumerate( + zip(field_schema["items"], document[field]) + ): + field_type = s.get("type") if field_type in app.data.serializers: - document[field][i] = \ - serialize_value(field_type, - document[field][i]) - elif 'valueschema' in field_schema: + document[field][i] = serialize_value( + field_type, document[field][i] + ) + elif "valueschema" in field_schema: # a valueschema - field_type = field_schema['valueschema']['type'] - if field_type == 'objectid': + field_type = field_schema["valueschema"]["type"] + if field_type == "objectid": target = document[field] for field in target: - target[field] = \ - serialize_value(field_type, target[field]) - elif field_type == 'dict': + target[field] = serialize_value( + field_type, target[field] + ) + elif field_type == "dict": for subdocument in document[field].values(): serialize( subdocument, - schema=field_schema - ['valueschema']['schema']) + schema=field_schema["valueschema"]["schema"], + ) elif field_type in app.data.serializers: # a simple field - document[field] = \ - serialize_value(field_type, document[field]) + document[field] = serialize_value(field_type, document[field]) return document @@ -543,8 +572,8 @@ def normalize_dotted_fields(document): normalize_dotted_fields(i) elif isinstance(document, dict): for field in list(document): - if '.' in field: - parts = field.split('.') + if "." in field: + parts = field.split(".") prev = document for part in parts[:-1]: if part not in prev: @@ -558,8 +587,7 @@ def normalize_dotted_fields(document): normalize_dotted_fields(document[field]) -def build_response_document( - document, resource, embedded_fields, latest_doc=None): +def build_response_document(document, resource, embedded_fields, latest_doc=None): """ Prepares a document for response including generation of ETag and metadata fields. @@ -584,33 +612,33 @@ def build_response_document( # Up to v0.4 etags were not stored with the documents. if config.IF_MATCH and config.ETAG not in document: - ignore_fields = resource_def['etag_ignore_fields'] - document[config.ETAG] = document_etag(document, - ignore_fields=ignore_fields) + ignore_fields = resource_def["etag_ignore_fields"] + document[config.ETAG] = document_etag(document, ignore_fields=ignore_fields) # hateoas links - if resource_def['hateoas'] and resource_def['id_field'] in document: + if resource_def["hateoas"] and resource_def["id_field"] in document: version = None - if resource_def['versioning'] is True \ - and request.args.get(config.VERSION_PARAM): + if resource_def["versioning"] is True and request.args.get( + config.VERSION_PARAM + ): version = document[config.VERSION] - self_dict = {'self': document_link(resource, - document[resource_def['id_field']], - version)} + self_dict = { + "self": document_link(resource, document[resource_def["id_field"]], version) + } if config.LINKS not in document: document[config.LINKS] = self_dict - elif 'self' not in document[config.LINKS]: + elif "self" not in document[config.LINKS]: document[config.LINKS].update(self_dict) # add version numbers - resolve_document_version(document, resource, 'GET', latest_doc) + resolve_document_version(document, resource, "GET", latest_doc) # resolve media resolve_media_files(document, resource) # resolve soft delete - if resource_def['soft_delete'] is True: + if resource_def["soft_delete"] is True: if document.get(config.DELETED) is None: document[config.DELETED] = False elif document[config.DELETED] is True: @@ -633,21 +661,21 @@ def field_definition(resource, chained_fields): .. versionadded 0.5 """ definition = config.DOMAIN[resource] - subfields = chained_fields.split('.') + subfields = chained_fields.split(".") for field in subfields: - if field not in definition.get('schema', {}): - if 'data_relation' in definition: - sub_resource = definition['data_relation']['resource'] + if field not in definition.get("schema", {}): + if "data_relation" in definition: + sub_resource = definition["data_relation"]["resource"] definition = config.DOMAIN[sub_resource] - if field not in definition['schema']: + if field not in definition["schema"]: return - definition = definition['schema'][field] - field_type = definition.get('type') - if field_type == 'list': - definition = definition['schema'] - elif field_type == 'objectid': + definition = definition["schema"][field] + field_type = definition.get("type") + if field_type == "list": + definition = definition["schema"] + elif field_type == "objectid": pass return definition @@ -672,33 +700,33 @@ def resolve_embedded_fields(resource, req): try: client_embedding = json.loads(req.embedded) except ValueError: - abort(400, description='Unable to parse `embedded` clause') + abort(400, description="Unable to parse `embedded` clause") # Build the list of fields where embedding is being requested try: - embedded_fields = [k for k, v in client_embedding.items() - if v == 1] - non_embedded_fields = [k for k, v in client_embedding.items() - if v == 0] + embedded_fields = [k for k, v in client_embedding.items() if v == 1] + non_embedded_fields = [k for k, v in client_embedding.items() if v == 0] except AttributeError: # We got something other than a dict - abort(400, description='Unable to parse `embedded` clause') + abort(400, description="Unable to parse `embedded` clause") embedded_fields = list( - (set(config.DOMAIN[resource]['embedded_fields']) | - set(embedded_fields)) - set(non_embedded_fields)) + (set(config.DOMAIN[resource]["embedded_fields"]) | set(embedded_fields)) + - set(non_embedded_fields) + ) # For each field, is the field allowed to be embedded? # Pick out fields that have a `data_relation` where `embeddable=True` enabled_embedded_fields = [] - for field in sorted(embedded_fields, key=lambda a: a.count('.')): + for field in sorted(embedded_fields, key=lambda a: a.count(".")): # Reject bogus field names field_def = field_definition(resource, field) if field_def: - if field_def.get('type') == 'list': - field_def = field_def['schema'] - if 'data_relation' in field_def and \ - field_def['data_relation'].get('embeddable'): + if field_def.get("type") == "list": + field_def = field_def["schema"] + if "data_relation" in field_def and field_def["data_relation"].get( + "embeddable" + ): # or could raise 400 here enabled_embedded_fields.append(field) @@ -724,41 +752,46 @@ def embedded_document(references, data_relation, field_name): references = [references] # Retrieve and serialize the requested document - if 'version' in data_relation and data_relation['version'] is True: + if "version" in data_relation and data_relation["version"] is True: # For the version flow, I keep the as-is logic (flow is too complex to # make it bulk) for reference in references: # grab the specific version - embedded_doc = get_data_version_relation_document( - data_relation, reference) + embedded_doc = get_data_version_relation_document(data_relation, reference) # grab the latest version latest_embedded_doc = get_data_version_relation_document( - data_relation, reference, latest=True) + data_relation, reference, latest=True + ) # make sure we got the documents if embedded_doc is None or latest_embedded_doc is None: # your database is not consistent!!! that is bad # TODO: we should notify the developers with a log. - abort(404, description=debug_error_message( - "Unable to locate embedded documents for '%s'" % - field_name - )) - - build_response_document(embedded_doc, data_relation['resource'], - [], latest_embedded_doc) + abort( + 404, + description=debug_error_message( + "Unable to locate embedded documents for '%s'" % field_name + ), + ) + + build_response_document( + embedded_doc, data_relation["resource"], [], latest_embedded_doc + ) embedded_docs.append(embedded_doc) else: - id_value_to_sort, list_of_id_field_name, subresources_query = \ - generate_query_and_sorting_criteria(data_relation, references) + id_value_to_sort, list_of_id_field_name, subresources_query = generate_query_and_sorting_criteria( + data_relation, references + ) for subresource in subresources_query: list_embedded_doc = list( - app.data.find(subresource, None, - subresources_query[subresource])) + app.data.find(subresource, None, subresources_query[subresource]) + ) if not list_embedded_doc: embedded_docs.extend( - [None] * len(subresources_query[subresource]["$or"])) + [None] * len(subresources_query[subresource]["$or"]) + ) else: for embedded_doc in list_embedded_doc: resolve_media_files(embedded_doc, subresource) @@ -769,8 +802,9 @@ def embedded_document(references, data_relation, field_name): # embedding of sub-documents - only in case the storage is not done via # DBref) if embedded_docs: - embedded_docs = sort_db_response(embedded_docs, id_value_to_sort, - list_of_id_field_name) + embedded_docs = sort_db_response( + embedded_docs, id_value_to_sort, list_of_id_field_name + ) if output_is_list: return embedded_docs @@ -794,12 +828,16 @@ def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): old_occurrence = 0 for id_field_name in set(list_of_id_field_name): - current_occurrence = old_occurrence + int(id_field_name_occurrences[ - id_field_name]) + current_occurrence = old_occurrence + int( + id_field_name_occurrences[id_field_name] + ) temp_embedded_docs.extend( - sort_per_resource(embedded_docs[old_occurrence:current_occurrence], - id_value_to_sort, - id_field_name)) + sort_per_resource( + embedded_docs[old_occurrence:current_occurrence], + id_value_to_sort, + id_field_name, + ) + ) old_occurrence = current_occurrence return temp_embedded_docs @@ -850,16 +888,21 @@ def generate_query_and_sorting_criteria(data_relation, references): for counter, reference in enumerate(references): # if reference is DBRef take the referenced collection as subresource # NOTE: using DBRef, I can define several resource for each link - subresource = reference.collection if isinstance(reference, DBRef) \ - else data_relation['resource'] + subresource = ( + reference.collection + if isinstance(reference, DBRef) + else data_relation["resource"] + ) if old_subresource and old_subresource != subresource: add_query_to_list(query, subresource, subresources_query) # NOTE: in case it is a DBRef link, the id_field_name is always the _id # regardless the Eve set-up - id_field_name = "_id" if isinstance(reference, DBRef) \ - else config.DOMAIN[subresource]['id_field'] - id_field_value = reference.id \ - if isinstance(reference, DBRef) else reference + id_field_name = ( + "_id" + if isinstance(reference, DBRef) + else config.DOMAIN[subresource]["id_field"] + ) + id_field_value = reference.id if isinstance(reference, DBRef) else reference query["$or"].append({id_field_name: id_field_value}) id_value_to_sort.append(id_field_value) list_of_id_field_name.append(id_field_name) @@ -890,8 +933,9 @@ def subdocuments(fields_chain, resource, document): subdocument = document[fields_chain[0]] docs = subdocument if isinstance(subdocument, list) else [subdocument] try: - resource = field_definition( - resource, fields_chain[0])['data_relation']['resource'] + resource = field_definition(resource, fields_chain[0])["data_relation"][ + "resource" + ] except KeyError: resource = resource @@ -935,10 +979,10 @@ def resolve_embedded_documents(document, resource, embedded_fields): .. versionadded:: 0.1.0 """ # NOTE(Gonéri): We resolve the embedded documents at the end. - for field in sorted(embedded_fields, key=lambda a: a.count('.')): - data_relation = field_definition(resource, field)['data_relation'] + for field in sorted(embedded_fields, key=lambda a: a.count(".")): + data_relation = field_definition(resource, field)["data_relation"] getter = lambda ref: embedded_document(ref, data_relation, field) # noqa - fields_chain = field.split('.') + fields_chain = field.split(".") last_field = fields_chain[-1] for subdocument in subdocuments(fields_chain[:-1], resource, document): if last_field not in subdocument: @@ -974,30 +1018,31 @@ def resolve_one_media(file_id, resource): if config.RETURN_MEDIA_AS_BASE64_STRING: ret_file = base64.encodestring(_file.read()) elif config.RETURN_MEDIA_AS_URL: - prefix = config.MEDIA_BASE_URL if config.MEDIA_BASE_URL \ - is not None else app.api_prefix - ret_file = '%s/%s/%s' % (prefix, config.MEDIA_ENDPOINT, - file_id) + prefix = ( + config.MEDIA_BASE_URL + if config.MEDIA_BASE_URL is not None + else app.api_prefix + ) + ret_file = "%s/%s/%s" % (prefix, config.MEDIA_ENDPOINT, file_id) else: ret_file = None if config.EXTENDED_MEDIA_INFO: - ret = { - 'file': ret_file, - } + ret = {"file": ret_file} # check if we should return any special fields for attribute in config.EXTENDED_MEDIA_INFO: if hasattr(_file, attribute): # add extended field if found in the file object - ret.update({ - attribute: getattr(_file, attribute) - }) + ret.update({attribute: getattr(_file, attribute)}) else: # tried to select an invalid attribute - abort(500, description=debug_error_message( - 'Invalid extended media attribute requested' - )) + abort( + 500, + description=debug_error_message( + "Invalid extended media attribute requested" + ), + ) return ret else: @@ -1018,18 +1063,18 @@ def marshal_write_response(document, resource): .. versionadded:: 0.4 """ - resource_def = app.config['DOMAIN'][resource] - if app.config['BANDWIDTH_SAVER'] is True: + resource_def = app.config["DOMAIN"][resource] + if app.config["BANDWIDTH_SAVER"] is True: # only return the automatic fields and special extra fields - fields = auto_fields(resource) + resource_def['extra_response_fields'] + fields = auto_fields(resource) + resource_def["extra_response_fields"] document = dict((k, v) for (k, v) in document.items() if k in fields) else: # avoid exposing the auth_field if it is not included in the # resource schema. - auth_field = resource_def.get('auth_field') - if auth_field and auth_field not in resource_def['schema']: + auth_field = resource_def.get("auth_field") + if auth_field and auth_field not in resource_def["schema"]: try: - del(document[auth_field]) + del (document[auth_field]) except: # 'auth_field' value has not been set by the auth class. pass @@ -1069,14 +1114,22 @@ def store_media_files(document, resource, original=None): if isinstance(document[field], list): id_lst = [] for stor_obj in document[field]: - id_lst.append(app.media.put( - stor_obj, filename=stor_obj.filename, - content_type=stor_obj.mimetype, resource=resource)) + id_lst.append( + app.media.put( + stor_obj, + filename=stor_obj.filename, + content_type=stor_obj.mimetype, + resource=resource, + ) + ) document[field] = id_lst else: document[field] = app.media.put( - document[field], filename=document[field].filename, - content_type=document[field].mimetype, resource=resource) + document[field], + filename=document[field].filename, + content_type=document[field].mimetype, + resource=resource, + ) def resource_media_fields(document, resource): @@ -1087,7 +1140,7 @@ def resource_media_fields(document, resource): .. versionadded:: 0.3 """ - media_fields = app.config['DOMAIN'][resource]['_media'] + media_fields = app.config["DOMAIN"][resource]["_media"] return [field for field in media_fields if field in document] @@ -1096,10 +1149,10 @@ def resolve_sub_resource_path(document, resource): return resource_def = config.DOMAIN[resource] - schema = resource_def['schema'] + schema = resource_def["schema"] fields = [] for field, value in request.view_args.items(): - if field in schema and field != resource_def['id_field']: + if field in schema and field != resource_def["id_field"]: fields.append(field) document[field] = value @@ -1123,9 +1176,9 @@ def resolve_user_restricted_access(document, resource): """ # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document - resource_def = app.config['DOMAIN'][resource] - auth = resource_def['authentication'] - auth_field = resource_def['auth_field'] + resource_def = app.config["DOMAIN"][resource] + auth = resource_def["authentication"] + auth_field = resource_def["auth_field"] if auth and auth_field: request_auth_value = auth.get_request_auth_value() if request_auth_value: @@ -1138,14 +1191,13 @@ def resolve_document_etag(documents, resource): .. versionadded:: 0.5 """ if config.IF_MATCH: - ignore_fields = config.DOMAIN[resource]['etag_ignore_fields'] + ignore_fields = config.DOMAIN[resource]["etag_ignore_fields"] if not isinstance(documents, list): documents = [documents] for document in documents: - document[config.ETAG] =\ - document_etag(document, ignore_fields=ignore_fields) + document[config.ETAG] = document_etag(document, ignore_fields=ignore_fields) def pre_event(f): @@ -1161,20 +1213,21 @@ def pre_event(f): .. versionadded:: 0.2 """ + @wraps(f) def decorated(*args, **kwargs): method = request.method - if method == 'HEAD': - method = 'GET' + if method == "HEAD": + method = "GET" - event_name = 'on_pre_' + method + event_name = "on_pre_" + method resource = args[0] if args else None gh_params = () rh_params = () - if method in ('GET', 'PATCH', 'DELETE', 'PUT'): + if method in ("GET", "PATCH", "DELETE", "PUT"): gh_params = (resource, request, kwargs) rh_params = (request, kwargs) - elif method in ('POST', ): + elif method in ("POST",): # POST hook does not support the kwargs argument gh_params = (resource, request) rh_params = (request,) @@ -1183,13 +1236,14 @@ def decorated(*args, **kwargs): getattr(app, event_name)(*gh_params) if resource: # resource hook - getattr(app, event_name + '_' + resource)(*rh_params) + getattr(app, event_name + "_" + resource)(*rh_params) combined_args = kwargs if len(args) > 1: combined_args.update(args[1].items()) r = f(resource, **combined_args) return r + return decorated @@ -1212,9 +1266,11 @@ def document_link(resource, document_id, version=None): .. versionchanged:: 0.0.3 Now returning a JSON link """ - version_part = '?version=%s' % version if version else '' - return {'title': '%s' % config.DOMAIN[resource]['item_title'], - 'href': '%s/%s%s' % (resource_link(), document_id, version_part)} + version_part = "?version=%s" % version if version else "" + return { + "title": "%s" % config.DOMAIN[resource]["item_title"], + "href": "%s/%s%s" % (resource_link(), document_id, version_part), + } def resource_link(): @@ -1229,18 +1285,18 @@ def resource_link(): .. versionadded:: 0.4 """ - path = request.path.strip('/') + path = request.path.strip("/") - if request.endpoint and '|item' in request.endpoint: - path = path[:path.rfind('/')] + if request.endpoint and "|item" in request.endpoint: + path = path[: path.rfind("/")] def strip_prefix(hit): - return path[len(hit):] if path.startswith(hit) else path + return path[len(hit) :] if path.startswith(hit) else path if config.URL_PREFIX: - path = strip_prefix(config.URL_PREFIX + '/') + path = strip_prefix(config.URL_PREFIX + "/") if config.API_VERSION: - path = strip_prefix(config.API_VERSION + '/') + path = strip_prefix(config.API_VERSION + "/") return path @@ -1276,9 +1332,11 @@ def oplog_push(resource, document, op, id=None): .. versionadded:: 0.5 """ - if not config.OPLOG \ - or op not in config.OPLOG_METHODS\ - or resource not in config.URLS: + if ( + not config.OPLOG + or op not in config.OPLOG_METHODS + or resource not in config.URLS + ): return resource_def = config.DOMAIN[resource] @@ -1294,10 +1352,13 @@ def oplog_push(resource, document, op, id=None): entries = [] for update in updates: entry = { - 'r': config.URLS[resource], - 'o': op, - 'i': (update[resource_def['id_field']] - if resource_def['id_field'] in update else id), + "r": config.URLS[resource], + "o": op, + "i": ( + update[resource_def["id_field"]] + if resource_def["id_field"] in update + else id + ), } if config.LAST_UPDATED in update: last_update = update[config.LAST_UPDATED] @@ -1305,19 +1366,19 @@ def oplog_push(resource, document, op, id=None): last_update = datetime.utcnow().replace(microsecond=0) entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update if config.OPLOG_AUDIT: - entry['ip'] = request.remote_addr + entry["ip"] = request.remote_addr - auth = resource_def['authentication'] - entry['u'] = auth.get_user_or_token() if auth else 'n/a' + auth = resource_def["authentication"] + entry["u"] = auth.get_user_or_token() if auth else "n/a" if op in config.OPLOG_CHANGE_METHODS: # these fields are already contained in 'entry'. - del(update[config.LAST_UPDATED]) + del (update[config.LAST_UPDATED]) # legacy documents (v0.4 or less) could be missing the etag # field if config.ETAG in update: - del(update[config.ETAG]) - entry['c'] = update + del (update[config.ETAG]) + entry["c"] = update else: pass diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 6fff91e3c..ad354203a 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -13,16 +13,25 @@ from flask import current_app as app, abort from eve.utils import config, ParsedRequest from eve.auth import requires_auth -from eve.methods.common import get_document, ratelimit, pre_event, \ - oplog_push, resolve_document_etag -from eve.versioning import versioned_id_field, resolve_document_version, \ - insert_versioning_documents, late_versioning_catch +from eve.methods.common import ( + get_document, + ratelimit, + pre_event, + oplog_push, + resolve_document_etag, +) +from eve.versioning import ( + versioned_id_field, + resolve_document_version, + insert_versioning_documents, + late_versioning_catch, +) from datetime import datetime import copy @ratelimit() -@requires_auth('item') +@requires_auth("item") @pre_event def deleteitem(resource, **lookup): """ @@ -37,8 +46,9 @@ def deleteitem(resource, **lookup): return deleteitem_internal(resource, concurrency_check=True, **lookup) -def deleteitem_internal(resource, concurrency_check=False, - suppress_callbacks=False, original=None, **lookup): +def deleteitem_internal( + resource, concurrency_check=False, suppress_callbacks=False, original=None, **lookup +): """ Intended for internal delete calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Deletes a resource item. @@ -83,10 +93,9 @@ def deleteitem_internal(resource, concurrency_check=False, Added the ``requires_auth`` decorator. """ resource_def = config.DOMAIN[resource] - soft_delete_enabled = resource_def['soft_delete'] + soft_delete_enabled = resource_def["soft_delete"] original = get_document(resource, concurrency_check, original, **lookup) - if not original or (soft_delete_enabled and - original.get(config.DELETED) is True): + if not original or (soft_delete_enabled and original.get(config.DELETED) is True): abort(404) # notify callbacks @@ -106,28 +115,28 @@ def deleteitem_internal(resource, concurrency_check=False, if config.IF_MATCH: resolve_document_etag(marked_document, resource) - resolve_document_version(marked_document, resource, 'DELETE', original) + resolve_document_version(marked_document, resource, "DELETE", original) # Update document in database (including version collection if needed) - id = original[resource_def['id_field']] + id = original[resource_def["id_field"]] try: app.data.replace(resource, id, marked_document, original) except app.data.OriginalChangedError: if concurrency_check: - abort(412, description='Client and server etags don\'t match') + abort(412, description="Client and server etags don't match") # create previous version if it wasn't already there late_versioning_catch(original, resource) # and add deleted version insert_versioning_documents(resource, marked_document) # update oplog if needed - oplog_push(resource, marked_document, 'DELETE', id) + oplog_push(resource, marked_document, "DELETE", id) else: # Delete the document for real # media cleanup - media_fields = app.config['DOMAIN'][resource]['_media'] + media_fields = app.config["DOMAIN"][resource]["_media"] # document might miss one or more media fields because of datasource # and/or client projection. @@ -149,19 +158,19 @@ def deleteitem_internal(resource, concurrency_check=False, else: app.media.delete(original[field], resource) - id = original[resource_def['id_field']] + id = original[resource_def["id_field"]] app.data.remove(resource, lookup) # TODO: should attempt to delete version collection even if setting is # off - if app.config['DOMAIN'][resource]['versioning'] is True: + if app.config["DOMAIN"][resource]["versioning"] is True: app.data.remove( resource + config.VERSIONS, - {versioned_id_field(resource_def): - original[resource_def['id_field']]}) + {versioned_id_field(resource_def): original[resource_def["id_field"]]}, + ) # update oplog if needed - oplog_push(resource, original, 'DELETE', id) + oplog_push(resource, original, "DELETE", id) if suppress_callbacks is not True: getattr(app, "on_deleted_item")(resource, original) @@ -170,7 +179,7 @@ def deleteitem_internal(resource, concurrency_check=False, return {}, None, None, 204 -@requires_auth('resource') +@requires_auth("resource") @pre_event def delete(resource, **lookup): """ Deletes all item of a resource (collection in MongoDB terms). Won't @@ -198,7 +207,7 @@ def delete(resource, **lookup): getattr(app, "on_delete_resource")(resource) getattr(app, "on_delete_resource_%s" % resource)() default_request = ParsedRequest() - if resource_def['soft_delete']: + if resource_def["soft_delete"]: # get_document should always fetch soft deleted documents from the db # callers must handle soft deleted documents default_request.show_deleted = True @@ -206,14 +215,11 @@ def delete(resource, **lookup): if not originals: abort(404) # I add new callback as I want the framework to be retro-compatible - getattr(app, "on_delete_resource_originals")(resource, - originals, - lookup) - getattr(app, "on_delete_resource_originals_%s" % resource)(originals, - lookup) - id_field = resource_def['id_field'] - - if resource_def['soft_delete']: + getattr(app, "on_delete_resource_originals")(resource, originals, lookup) + getattr(app, "on_delete_resource_originals_%s" % resource)(originals, lookup) + id_field = resource_def["id_field"] + + if resource_def["soft_delete"]: # I need to check that I have at least some documents not soft_deleted # Otherwise, I should abort 404 # I skip all the soft_deleted documents @@ -223,9 +229,13 @@ def delete(resource, **lookup): abort(404) for document in originals: lookup[id_field] = document[id_field] - deleteitem_internal(resource, concurrency_check=False, - suppress_callbacks=True, - original=document, **lookup) + deleteitem_internal( + resource, + concurrency_check=False, + suppress_callbacks=True, + original=document, + **lookup + ) else: # TODO if the resource schema includes media files, these won't be # deleted by use of this global method (it should be disabled). Media @@ -235,7 +245,7 @@ def delete(resource, **lookup): # TODO: should attempt to delete version collection even if setting is # off - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: app.data.remove(resource + config.VERSIONS, lookup) getattr(app, "on_deleted_resource")(resource) diff --git a/eve/methods/get.py b/eve/methods/get.py index d0e946270..a444e10bb 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -17,16 +17,28 @@ from flask import current_app as app, abort, request from werkzeug import MultiDict -from .common import ratelimit, epoch, pre_event, resolve_embedded_fields, \ - build_response_document, resource_link, document_link, last_updated +from .common import ( + ratelimit, + epoch, + pre_event, + resolve_embedded_fields, + build_response_document, + resource_link, + document_link, + last_updated, +) from eve.auth import requires_auth from eve.utils import parse_request, home_link, querydef, config -from eve.versioning import synthesize_versioned_document, versioned_id_field, \ - get_old_document, diff_document +from eve.versioning import ( + synthesize_versioned_document, + versioned_id_field, + get_old_document, + diff_document, +) @ratelimit() -@requires_auth('resource') +@requires_auth("resource") @pre_event def get(resource, **lookup): """ @@ -102,12 +114,13 @@ def get_internal(resource, **lookup): JSON formatted. """ - datasource = config.DOMAIN[resource]['datasource'] - aggregation = datasource.get('aggregation') + datasource = config.DOMAIN[resource]["datasource"] + aggregation = datasource.get("aggregation") if aggregation: - return _perform_aggregation(resource, aggregation['pipeline'], - aggregation['options']) + return _perform_aggregation( + resource, aggregation["pipeline"], aggregation["options"] + ) else: return _perform_find(resource, lookup) @@ -152,10 +165,10 @@ def parse_again(st_value, key, value): try: query = json.loads(req.aggregation) except ValueError: - abort(400, description='Aggregation query could not be parsed.') + abort(400, description="Aggregation query could not be parsed.") for key, value in query.items(): - if key[0] != '$': + if key[0] != "$": pass for stage in req_pipeline: parse_aggregation_stage(stage, key, value) @@ -223,11 +236,11 @@ def _perform_find(resource, lookup): count = cursor.count(with_limit_and_skip=False) headers.append((config.HEADER_TOTAL_COUNT, count)) - if config.DOMAIN[resource]['hateoas']: + if config.DOMAIN[resource]["hateoas"]: response[config.LINKS] = _pagination_links(resource, req, count) # add pagination info - if config.DOMAIN[resource]['pagination']: + if config.DOMAIN[resource]["pagination"]: response[config.META] = _meta_links(req, count) # notify registered callback functions. Please note that, should the @@ -240,14 +253,14 @@ def _perform_find(resource, lookup): # the 'extra' cursor field, if present, will be added to the response. # Can be used by Eve extensions to add extra, custom data to any # response. - if hasattr(cursor, 'extra'): - getattr(cursor, 'extra')(response) + if hasattr(cursor, "extra"): + getattr(cursor, "extra")(response) return response, last_modified, etag, status, headers @ratelimit() -@requires_auth('item') +@requires_auth("item") @pre_event def getitem(resource, **lookup): """ @@ -321,7 +334,7 @@ def getitem_internal(resource, **lookup): resource_def = config.DOMAIN[resource] embedded_fields = resolve_embedded_fields(resource, req) - soft_delete_enabled = config.DOMAIN[resource]['soft_delete'] + soft_delete_enabled = config.DOMAIN[resource]["soft_delete"] if soft_delete_enabled: # GET requests should always fetch soft deleted documents from the db # They are handled and included in 404 responses below. @@ -342,16 +355,15 @@ def getitem_internal(resource, **lookup): last_modified = last_updated(document) # synthesize old document version(s) - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: latest_doc = document - document = get_old_document( - resource, req, lookup, document, version) + document = get_old_document(resource, req, lookup, document, version) # meld into response document build_response_document(document, resource, embedded_fields, latest_doc) if config.IF_MATCH: etag = document[config.ETAG] - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: # In order to keep the LATEST_VERSION field up to date in client # caches, changes to the latest version should invalidate cached # copies of previous verisons. Incorporate the latest version into @@ -372,21 +384,20 @@ def getitem_internal(resource, **lookup): # facilitate client caching by returning a 304 when appropriate cache_validators = {True: 0, False: 0} if req.if_modified_since: - cache_valid = (last_modified <= req.if_modified_since) + cache_valid = last_modified <= req.if_modified_since cache_validators[cache_valid] += 1 if req.if_none_match: - cache_valid = (etag == req.if_none_match) + cache_valid = etag == req.if_none_match cache_validators[cache_valid] += 1 # If all cache validators are true, return 304 if (cache_validators[True] > 0) and (cache_validators[False] == 0): return {}, last_modified, etag, 304 - if version == 'all' or version == 'diffs': + if version == "all" or version == "diffs": # find all versions - lookup[versioned_id_field(resource_def)] \ - = lookup[resource_def['id_field']] - del lookup[resource_def['id_field']] - if version == 'diffs' or req.sort is None: + lookup[versioned_id_field(resource_def)] = lookup[resource_def["id_field"]] + del lookup[resource_def["id_field"]] + if version == "diffs" or req.sort is None: # default sort for 'all', required sort for 'diffs' req.sort = '[("%s", 1)]' % config.VERSION req.if_modified_since = None # we always want the full history here @@ -402,70 +413,72 @@ def getitem_internal(resource, **lookup): last_document = {} # if we aren't starting on page 1, then we need to init last_doc - if version == 'diffs' and req.page > 1: + if version == "diffs" and req.page > 1: # grab the last document on the previous page to diff from - last_version = cursor[0][app.config['VERSION']] - 1 + last_version = cursor[0][app.config["VERSION"]] - 1 last_document = get_old_document( - resource, req, lookup, latest_doc, last_version) + resource, req, lookup, latest_doc, last_version + ) for i, document in enumerate(cursor): document = synthesize_versioned_document( - latest_doc, document, resource_def) - build_response_document( - document, resource, embedded_fields, latest_doc) - if version == 'diffs': + latest_doc, document, resource_def + ) + build_response_document(document, resource, embedded_fields, latest_doc) + if version == "diffs": if i == 0: documents.append(document) else: - documents.append(diff_document( - resource_def, last_document, document)) + documents.append( + diff_document(resource_def, last_document, document) + ) last_document = document else: documents.append(document) # add documents to response - if config.DOMAIN[resource]['hateoas']: + if config.DOMAIN[resource]["hateoas"]: response[config.ITEMS] = documents else: response = documents elif soft_delete_enabled and document.get(config.DELETED) is True: # This document was soft deleted. Respond with 404 and the deleted # version of the document. - document[config.STATUS] = config.STATUS_ERR, + document[config.STATUS] = (config.STATUS_ERR,) document[config.ERROR] = { - 'code': 404, - 'message': 'The requested URL was not found on this server.' + "code": 404, + "message": "The requested URL was not found on this server.", } return document, last_modified, etag, 404 else: response = document # extra hateoas links - if config.DOMAIN[resource]['hateoas']: + if config.DOMAIN[resource]["hateoas"]: # use the id of the latest document for multi-document requests if cursor: count = cursor.count(with_limit_and_skip=False) - response[config.LINKS] = \ - _pagination_links(resource, req, count, - latest_doc[resource_def['id_field']]) - if config.DOMAIN[resource]['pagination']: + response[config.LINKS] = _pagination_links( + resource, req, count, latest_doc[resource_def["id_field"]] + ) + if config.DOMAIN[resource]["pagination"]: response[config.META] = _meta_links(req, count) else: - response[config.LINKS] = \ - _pagination_links(resource, req, None, - response[resource_def['id_field']]) + response[config.LINKS] = _pagination_links( + resource, req, None, response[resource_def["id_field"]] + ) # callbacks not supported on version diffs because of partial documents - if version != 'diffs': + if version != "diffs": # TODO: callbacks not currently supported with ?version=all # notify registered callback functions. Please note that, should # the functions modify the document, last_modified and etag # won't be updated to reflect the changes (they always reflect the # documents state on the database). - if resource_def['versioning'] is True and version == 'all': + if resource_def["versioning"] is True and version == "all": versions = response - if config.DOMAIN[resource]['hateoas']: + if config.DOMAIN[resource]["hateoas"]: versions = response[config.ITEMS] for version_item in versions: getattr(app, "on_fetched_item")(resource, version_item) @@ -509,66 +522,93 @@ def _pagination_links(resource, req, document_count, document_id=None): JSON links """ version = None - if config.DOMAIN[resource]['versioning'] is True: + if config.DOMAIN[resource]["versioning"] is True: version = request.args.get(config.VERSION_PARAM) other_params = _other_params(req.args) # construct the default links - q = querydef(req.max_results, req.where, req.sort, version, req.page, - other_params) - resource_title = config.DOMAIN[resource]['resource_title'] - _links = {'parent': home_link(), - 'self': {'title': resource_title, - 'href': resource_link()}} + q = querydef(req.max_results, req.where, req.sort, version, req.page, other_params) + resource_title = config.DOMAIN[resource]["resource_title"] + _links = { + "parent": home_link(), + "self": {"title": resource_title, "href": resource_link()}, + } # change links if document ID is given if document_id: - _links['self'] = document_link(resource, document_id) - _links['collection'] = {'title': resource_title, - 'href': '%s%s' % (resource_link(), q)} + _links["self"] = document_link(resource, document_id) + _links["collection"] = { + "title": resource_title, + "href": "%s%s" % (resource_link(), q), + } # make more specific links for versioned requests - if version in ('all', 'diffs'): - _links['parent'] = {'title': resource_title, - 'href': resource_link()} - _links['collection'] = document_link(resource, document_id) + if version in ("all", "diffs"): + _links["parent"] = {"title": resource_title, "href": resource_link()} + _links["collection"] = document_link(resource, document_id) elif version: - _links['parent'] = document_link(resource, document_id) - _links['collection'] = {'title': resource_title, - 'href': '%s?version=all' - % _links['parent']['href']} + _links["parent"] = document_link(resource, document_id) + _links["collection"] = { + "title": resource_title, + "href": "%s?version=all" % _links["parent"]["href"], + } # modify the self link to add query params or version number if document_count: - _links['self']['href'] = '%s%s' % (_links['self']['href'], q) - elif not document_count and version and version not in ('all', 'diffs'): - _links['self'] = document_link(resource, document_id, version) + _links["self"]["href"] = "%s%s" % (_links["self"]["href"], q) + elif not document_count and version and version not in ("all", "diffs"): + _links["self"] = document_link(resource, document_id, version) # create pagination links - if config.DOMAIN[resource]['pagination']: + if config.DOMAIN[resource]["pagination"]: # strip any queries from the self link if present - _pagination_link = _links['self']['href'].split('?')[0] - - if (req.page * req.max_results < (document_count or 0) or - config.OPTIMIZE_PAGINATION_FOR_SPEED): - q = querydef(req.max_results, req.where, req.sort, version, - req.page + 1, other_params) - _links['next'] = {'title': 'next page', 'href': '%s%s' % - (_pagination_link, q)} + _pagination_link = _links["self"]["href"].split("?")[0] + + if ( + req.page * req.max_results < (document_count or 0) + or config.OPTIMIZE_PAGINATION_FOR_SPEED + ): + q = querydef( + req.max_results, + req.where, + req.sort, + version, + req.page + 1, + other_params, + ) + _links["next"] = { + "title": "next page", + "href": "%s%s" % (_pagination_link, q), + } if document_count: - last_page = int(math.ceil(document_count / float( - req.max_results))) - q = querydef(req.max_results, req.where, req.sort, version, - last_page, other_params) - _links['last'] = {'title': 'last page', 'href': '%s%s' % ( - _pagination_link, q)} + last_page = int(math.ceil(document_count / float(req.max_results))) + q = querydef( + req.max_results, + req.where, + req.sort, + version, + last_page, + other_params, + ) + _links["last"] = { + "title": "last page", + "href": "%s%s" % (_pagination_link, q), + } if req.page > 1: - q = querydef(req.max_results, req.where, req.sort, version, - req.page - 1, other_params) - _links['prev'] = {'title': 'previous page', 'href': '%s%s' % - (_pagination_link, q)} + q = querydef( + req.max_results, + req.where, + req.sort, + version, + req.page - 1, + other_params, + ) + _links["prev"] = { + "title": "previous page", + "href": "%s%s" % (_pagination_link, q), + } return _links @@ -578,11 +618,20 @@ def _other_params(args): :param args: multidict containing the request parameters """ - default_params = [config.QUERY_WHERE, config.QUERY_SORT, - config.QUERY_PAGE, config.QUERY_MAX_RESULTS, - config.QUERY_EMBEDDED, config.QUERY_PROJECTION] - return MultiDict((key, value) for key, values in args.lists() - for value in values if key not in default_params) + default_params = [ + config.QUERY_WHERE, + config.QUERY_SORT, + config.QUERY_PAGE, + config.QUERY_MAX_RESULTS, + config.QUERY_EMBEDDED, + config.QUERY_PROJECTION, + ] + return MultiDict( + (key, value) + for key, values in args.lists() + for value in values + if key not in default_params + ) def _meta_links(req, count): @@ -593,10 +642,7 @@ def _meta_links(req, count): .. versionadded:: 0.5 """ - meta = { - config.QUERY_PAGE: req.page, - config.QUERY_MAX_RESULTS: req.max_results, - } + meta = {config.QUERY_PAGE: req.page, config.QUERY_MAX_RESULTS: req.max_results} if config.OPTIMIZE_PAGINATION_FOR_SPEED is False: - meta['total'] = count + meta["total"] = count return meta diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 553864209..5e5acf6e1 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -17,16 +17,28 @@ from eve.utils import config, debug_error_message, parse_request from eve.auth import requires_auth from eve.validation import DocumentError -from eve.methods.common import get_document, parse, payload as payload_, \ - ratelimit, pre_event, store_media_files, resolve_embedded_fields, \ - build_response_document, marshal_write_response, resolve_document_etag, \ - oplog_push -from eve.versioning import resolve_document_version, \ - insert_versioning_documents, late_versioning_catch +from eve.methods.common import ( + get_document, + parse, + payload as payload_, + ratelimit, + pre_event, + store_media_files, + resolve_embedded_fields, + build_response_document, + marshal_write_response, + resolve_document_etag, + oplog_push, +) +from eve.versioning import ( + resolve_document_version, + insert_versioning_documents, + late_versioning_catch, +) @ratelimit() -@requires_auth('item') +@requires_auth("item") @pre_event def patch(resource, payload=None, **lookup): """ @@ -37,12 +49,14 @@ def patch(resource, payload=None, **lookup): .. versionchanged:: 0.5 Split into patch() and patch_internal(). """ - return patch_internal(resource, payload, concurrency_check=True, - skip_validation=False, **lookup) + return patch_internal( + resource, payload, concurrency_check=True, skip_validation=False, **lookup + ) -def patch_internal(resource, payload=None, concurrency_check=False, - skip_validation=False, **lookup): +def patch_internal( + resource, payload=None, concurrency_check=False, skip_validation=False, **lookup +): """ Intended for internal patch calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Performs a document patch/update. @@ -136,11 +150,11 @@ def patch_internal(resource, payload=None, concurrency_check=False, # not found abort(404) - resource_def = app.config['DOMAIN'][resource] - schema = resource_def['schema'] + resource_def = app.config["DOMAIN"][resource] + schema = resource_def["schema"] validator = app.validator(schema, resource=resource) - object_id = original[resource_def['id_field']] + object_id = original[resource_def["id_field"]] last_modified = None etag = None @@ -158,8 +172,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, if skip_validation: validation = True else: - validation = validator.validate_update(updates, object_id, - original) + validation = validator.validate_update(updates, object_id, original) updates = validator.document if validation: @@ -169,13 +182,12 @@ def patch_internal(resource, payload=None, concurrency_check=False, late_versioning_catch(original, resource) store_media_files(updates, resource, original) - resolve_document_version(updates, resource, 'PATCH', original) + resolve_document_version(updates, resource, "PATCH", original) # some datetime precision magic - updates[config.LAST_UPDATED] = \ - datetime.utcnow().replace(microsecond=0) + updates[config.LAST_UPDATED] = datetime.utcnow().replace(microsecond=0) - if resource_def['soft_delete'] is True: + if resource_def["soft_delete"] is True: # PATCH with soft delete enabled should always set the DELETED # field to False. We are either carrying through un-deleted # status, or restoring a soft deleted document @@ -193,7 +205,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, getattr(app, "on_update")(resource, updates, original) getattr(app, "on_update_%s" % resource)(updates, original) - if resource_def['merge_nested_documents']: + if resource_def["merge_nested_documents"]: updates = resolve_nested_documents(updates, updated) updated.update(updates) @@ -202,11 +214,10 @@ def patch_internal(resource, payload=None, concurrency_check=False, # now storing the (updated) ETAG with every document (#453) updates[config.ETAG] = updated[config.ETAG] - app.data.update( - resource, object_id, updates, original) + app.data.update(resource, object_id, updates, original) # update oplog if needed - oplog_push(resource, updates, 'PATCH', object_id) + oplog_push(resource, updates, "PATCH", object_id) insert_versioning_documents(resource, updated) @@ -217,8 +228,7 @@ def patch_internal(resource, payload=None, concurrency_check=False, updated.update(updates) # build the full response document - build_response_document( - updated, resource, embedded_fields, updated) + build_response_document(updated, resource, embedded_fields, updated) response = updated if config.IF_MATCH: etag = response[config.ETAG] @@ -227,15 +237,13 @@ def patch_internal(resource, payload=None, concurrency_check=False, except DocumentError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) - issues['validator exception'] = str(e) + issues["validator exception"] = str(e) except exceptions.HTTPException as e: raise e except Exception as e: # consider all other exceptions as Bad Requests app.logger.exception(e) - abort(400, description=debug_error_message( - 'An exception occurred: %s' % e - )) + abort(400, description=debug_error_message("An exception occurred: %s" % e)) if len(issues): response[config.ISSUES] = issues diff --git a/eve/methods/post.py b/eve/methods/post.py index 59a1b56df..cfcb20f5e 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -16,16 +16,26 @@ from eve.utils import config, parse_request, debug_error_message from eve.auth import requires_auth from eve.validation import DocumentError -from eve.methods.common import parse, payload, ratelimit, \ - pre_event, store_media_files, resolve_user_restricted_access, \ - resolve_embedded_fields, build_response_document, marshal_write_response, \ - resolve_sub_resource_path, resolve_document_etag, oplog_push, resource_link -from eve.versioning import resolve_document_version, \ - insert_versioning_documents +from eve.methods.common import ( + parse, + payload, + ratelimit, + pre_event, + store_media_files, + resolve_user_restricted_access, + resolve_embedded_fields, + build_response_document, + marshal_write_response, + resolve_sub_resource_path, + resolve_document_etag, + oplog_push, + resource_link, +) +from eve.versioning import resolve_document_version, insert_versioning_documents @ratelimit() -@requires_auth('resource') +@requires_auth("resource") @pre_event def post(resource, payl=None): """ @@ -148,14 +158,13 @@ def post_internal(resource, payl=None, skip_validation=False): """ date_utc = datetime.utcnow().replace(microsecond=0) - resource_def = app.config['DOMAIN'][resource] - schema = resource_def['schema'] - validator = None if skip_validation \ - else app.validator(schema, resource=resource) + resource_def = app.config["DOMAIN"][resource] + schema = resource_def["schema"] + validator = None if skip_validation else app.validator(schema, resource=resource) documents = [] results = [] failures = 0 - id_field = resource_def['id_field'] + id_field = resource_def["id_field"] if config.BANDWIDTH_SAVER is True: embedded_fields = [] @@ -172,14 +181,10 @@ def post_internal(resource, payl=None, skip_validation=False): if not payl: # empty bulk insert - abort(400, description=debug_error_message( - 'Empty bulk insert' - )) + abort(400, description=debug_error_message("Empty bulk insert")) - if len(payl) > 1 and not config.DOMAIN[resource]['bulk_enabled']: - abort(400, description=debug_error_message( - 'Bulk insert not allowed' - )) + if len(payl) > 1 and not config.DOMAIN[resource]["bulk_enabled"]: + abort(400, description=debug_error_message("Bulk insert not allowed")) for value in payl: document = [] @@ -198,31 +203,27 @@ def post_internal(resource, payl=None, skip_validation=False): document = validator.document # Populate meta and default fields - document[config.LAST_UPDATED] = \ - document[config.DATE_CREATED] = date_utc + document[config.LAST_UPDATED] = document[config.DATE_CREATED] = date_utc - if config.DOMAIN[resource]['soft_delete'] is True: + if config.DOMAIN[resource]["soft_delete"] is True: document[config.DELETED] = False resolve_user_restricted_access(document, resource) store_media_files(document, resource) - resolve_document_version(document, resource, 'POST') + resolve_document_version(document, resource, "POST") else: # validation errors added to list of document issues doc_issues = validator.errors except DocumentError as e: - doc_issues['validation exception'] = str(e) + doc_issues["validation exception"] = str(e) except Exception as e: # most likely a problem with the incoming payload, report back to # the client as if it was a validation issue app.logger.exception(e) - doc_issues['exception'] = str(e) + doc_issues["exception"] = str(e) if len(doc_issues): - document = { - config.STATUS: config.STATUS_ERR, - config.ISSUES: doc_issues, - } + document = {config.STATUS: config.STATUS_ERR, config.ISSUES: doc_issues} failures += 1 documents.append(document) @@ -231,8 +232,10 @@ def post_internal(resource, payl=None, skip_validation=False): # If at least one document got issues, the whole request fails and a # ``422 Bad Request`` status is return. for document in documents: - if config.STATUS in document \ - and document[config.STATUS] == config.STATUS_ERR: + if ( + config.STATUS in document + and document[config.STATUS] == config.STATUS_ERR + ): results.append(document) else: results.append({config.STATUS: config.STATUS_OK}) @@ -250,7 +253,7 @@ def post_internal(resource, payl=None, skip_validation=False): ids = app.data.insert(resource, documents) # update oplog if needed - oplog_push(resource, documents, 'POST') + oplog_push(resource, documents, "POST") # assign document ids for document in documents: @@ -261,8 +264,7 @@ def post_internal(resource, payl=None, skip_validation=False): # build the full response document result = document - build_response_document( - result, resource, embedded_fields, document) + build_response_document(result, resource, embedded_fields, document) # add extra write meta data result[config.STATUS] = config.STATUS_OK @@ -297,7 +299,10 @@ def post_internal(resource, payl=None, skip_validation=False): % failures, } - location_header = None if return_code != 201 or not documents else \ - [('Location', '%s/%s' % (resource_link(), documents[0][id_field]))] + location_header = ( + None + if return_code != 201 or not documents + else [("Location", "%s/%s" % (resource_link(), documents[0][id_field]))] + ) return response, None, None, return_code, location_header diff --git a/eve/methods/put.py b/eve/methods/put.py index e57dbdc55..86cba5ffd 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -15,19 +15,33 @@ from werkzeug import exceptions from eve.auth import auth_field_and_value, requires_auth -from eve.methods.common import get_document, parse, payload as payload_, \ - ratelimit, pre_event, store_media_files, resolve_user_restricted_access, \ - resolve_embedded_fields, build_response_document, marshal_write_response, \ - resolve_sub_resource_path, resolve_document_etag, oplog_push +from eve.methods.common import ( + get_document, + parse, + payload as payload_, + ratelimit, + pre_event, + store_media_files, + resolve_user_restricted_access, + resolve_embedded_fields, + build_response_document, + marshal_write_response, + resolve_sub_resource_path, + resolve_document_etag, + oplog_push, +) from eve.methods.post import post_internal from eve.utils import config, debug_error_message, parse_request from eve.validation import DocumentError -from eve.versioning import resolve_document_version, \ - insert_versioning_documents, late_versioning_catch +from eve.versioning import ( + resolve_document_version, + insert_versioning_documents, + late_versioning_catch, +) @ratelimit() -@requires_auth('item') +@requires_auth("item") @pre_event def put(resource, payload=None, **lookup): """ @@ -38,12 +52,14 @@ def put(resource, payload=None, **lookup): .. versionchanged:: 0.5 Split into put() and put_internal(). """ - return put_internal(resource, payload, concurrency_check=True, - skip_validation=False, **lookup) + return put_internal( + resource, payload, concurrency_check=True, skip_validation=False, **lookup + ) -def put_internal(resource, payload=None, concurrency_check=False, - skip_validation=False, **lookup): +def put_internal( + resource, payload=None, concurrency_check=False, skip_validation=False, **lookup +): """ Intended for internal put calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Performs a document replacement. @@ -106,8 +122,8 @@ def put_internal(resource, payload=None, concurrency_check=False, .. versionadded:: 0.1.0 """ - resource_def = app.config['DOMAIN'][resource] - schema = resource_def['schema'] + resource_def = app.config["DOMAIN"][resource] + schema = resource_def["schema"] validator = app.validator(schema, resource=resource) if payload is None: @@ -117,17 +133,21 @@ def put_internal(resource, payload=None, concurrency_check=False, # but returning the document owner in the projection. This allows us to # prevent PUT if the document exists, but is owned by a different user # than the currently authenticated one. - original = get_document(resource, concurrency_check, - check_auth_value=False, - force_auth_field_projection=True, **lookup) + original = get_document( + resource, + concurrency_check, + check_auth_value=False, + force_auth_field_projection=True, + **lookup + ) if not original: if config.UPSERT_ON_PUT: - id = lookup[resource_def['id_field']] + id = lookup[resource_def["id_field"]] # this guard avoids a bson dependency, which would be needed if we # wanted to use 'isinstance'. Should also be slightly faster. - if schema[resource_def['id_field']].get('type', '') == 'objectid': + if schema[resource_def["id_field"]].get("type", "") == "objectid": id = str(id) - payload[resource_def['id_field']] = id + payload[resource_def["id_field"]] = id return post_internal(resource, payl=payload) else: abort(404) @@ -141,7 +161,7 @@ def put_internal(resource, payload=None, concurrency_check=False, last_modified = None etag = None issues = {} - object_id = original[resource_def['id_field']] + object_id = original[resource_def["id_field"]] response = {} @@ -157,8 +177,7 @@ def put_internal(resource, payload=None, concurrency_check=False, if skip_validation: validation = True else: - validation = validator.validate_replace(document, object_id, - original) + validation = validator.validate_replace(document, object_id, original) # Apply coerced values document = validator.document @@ -170,7 +189,7 @@ def put_internal(resource, payload=None, concurrency_check=False, last_modified = datetime.utcnow().replace(microsecond=0) document[config.LAST_UPDATED] = last_modified document[config.DATE_CREATED] = original[config.DATE_CREATED] - if resource_def['soft_delete'] is True: + if resource_def["soft_delete"] is True: # PUT with soft delete enabled should always set the DELETED # field to False. We are either carrying through un-deleted # status, or restoring a soft deleted document @@ -179,12 +198,12 @@ def put_internal(resource, payload=None, concurrency_check=False, # id_field not in document means it is not being automatically # handled (it has been set to a field which exists in the # resource schema. - if resource_def['id_field'] not in document: - document[resource_def['id_field']] = object_id + if resource_def["id_field"] not in document: + document[resource_def["id_field"]] = object_id resolve_user_restricted_access(document, resource) store_media_files(document, resource, original) - resolve_document_version(document, resource, 'PUT', original) + resolve_document_version(document, resource, "PUT", original) # notify callbacks getattr(app, "on_replace")(resource, document, original) @@ -194,15 +213,13 @@ def put_internal(resource, payload=None, concurrency_check=False, # write to db try: - app.data.replace( - resource, object_id, document, original) + app.data.replace(resource, object_id, document, original) except app.data.OriginalChangedError: if concurrency_check: - abort(412, - description='Client and server etags don\'t match') + abort(412, description="Client and server etags don't match") # update oplog if needed - oplog_push(resource, document, 'PUT') + oplog_push(resource, document, "PUT") insert_versioning_documents(resource, document) @@ -211,8 +228,7 @@ def put_internal(resource, payload=None, concurrency_check=False, getattr(app, "on_replaced_%s" % resource)(document, original) # build the full response document - build_response_document( - document, resource, embedded_fields, document) + build_response_document(document, resource, embedded_fields, document) response = document if config.IF_MATCH: etag = response[config.ETAG] @@ -221,15 +237,13 @@ def put_internal(resource, payload=None, concurrency_check=False, except DocumentError as e: # TODO should probably log the error and abort 400 instead (when we # got logging) - issues['validator exception'] = str(e) + issues["validator exception"] = str(e) except exceptions.HTTPException as e: raise e except Exception as e: # consider all other exceptions as Bad Requests app.logger.exception(e) - abort(400, description=debug_error_message( - 'An exception occurred: %s' % e - )) + abort(400, description=debug_error_message("An exception occurred: %s" % e)) if len(issues): response[config.ISSUES] = issues diff --git a/eve/render.py b/eve/render.py index c9612509e..8babaf807 100644 --- a/eve/render.py +++ b/eve/render.py @@ -17,8 +17,13 @@ from werkzeug import utils from functools import wraps from eve.methods.common import get_rate_limit -from eve.utils import date_to_str, date_to_rfc1123, config, \ - debug_error_message, import_from_string +from eve.utils import ( + date_to_str, + date_to_rfc1123, + config, + debug_error_message, + import_from_string, +) from flask import make_response, request, Response, current_app as app, abort from collections import OrderedDict # noqa @@ -41,19 +46,21 @@ def raise_event(f): .. versionadded:: 0.0.6 """ + @wraps(f) def decorated(*args, **kwargs): r = f(*args, **kwargs) method = request.method - if method in ('GET', 'POST', 'PATCH', 'DELETE', 'PUT'): - event_name = 'on_post_' + method + if method in ("GET", "POST", "PATCH", "DELETE", "PUT"): + event_name = "on_post_" + method resource = args[0] if args else None # general hook getattr(app, event_name)(resource, request, r) if resource: # resource hook - getattr(app, event_name + '_' + resource)(request, r) + getattr(app, event_name + "_" + resource)(request, r) return r + return decorated @@ -85,8 +92,9 @@ def send_response(resource, response): return _prepare_response(resource, *response if response else [None]) -def _prepare_response(resource, dct, last_modified=None, etag=None, - status=200, headers=None): +def _prepare_response( + resource, dct, last_modified=None, etag=None, status=200, headers=None +): """ Prepares the response object according to the client request and available renderers, making sure that all accessory directives (caching, etag, last-modified) are present. @@ -128,7 +136,7 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, .. versionadded:: 0.0.4 """ - if request.method == 'OPTIONS': + if request.method == "OPTIONS": resp = app.make_default_options_response() else: # obtain the best match between client's request and available mime @@ -141,7 +149,7 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, # JSONP if config.JSONP_ARGUMENT: jsonp_arg = config.JSONP_ARGUMENT - if jsonp_arg in request.args and 'json' in mime: + if jsonp_arg in request.args and "json" in mime: callback = request.args.get(jsonp_arg) rendered = "%s(%s)" % (callback, rendered) @@ -152,30 +160,30 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, # extra headers if headers: for header, value in headers: - if header != 'Content-Type': + if header != "Content-Type": resp.headers.add(header, value) # cache directives - if request.method in ('GET', 'HEAD'): + if request.method in ("GET", "HEAD"): if resource: - cache_control = config.DOMAIN[resource]['cache_control'] - expires = config.DOMAIN[resource]['cache_expires'] + cache_control = config.DOMAIN[resource]["cache_control"] + expires = config.DOMAIN[resource]["cache_expires"] else: cache_control = config.CACHE_CONTROL expires = config.CACHE_EXPIRES if cache_control: - resp.headers.add('Cache-Control', cache_control) + resp.headers.add("Cache-Control", cache_control) if expires: resp.expires = time.time() + expires # etag and last-modified if etag: - resp.headers.add('ETag', '"' + etag + '"') + resp.headers.add("ETag", '"' + etag + '"') if last_modified: - resp.headers.add('Last-Modified', date_to_rfc1123(last_modified)) + resp.headers.add("Last-Modified", date_to_rfc1123(last_modified)) # CORS - origin = request.headers.get('Origin') + origin = request.headers.get("Origin") if origin and (config.X_DOMAINS or config.X_DOMAINS_RE): if config.X_DOMAINS is None: domains = [] @@ -217,31 +225,30 @@ def _prepare_response(resource, dct, last_modified=None, etag=None, # is "true" allow_credentials = config.X_ALLOW_CREDENTIALS is True - methods = app.make_default_options_response().headers.get('allow', '') + methods = app.make_default_options_response().headers.get("allow", "") - if '*' in domains: - resp.headers.add('Access-Control-Allow-Origin', origin) - resp.headers.add('Vary', 'Origin') + if "*" in domains: + resp.headers.add("Access-Control-Allow-Origin", origin) + resp.headers.add("Vary", "Origin") elif any(origin == domain for domain in domains): - resp.headers.add('Access-Control-Allow-Origin', origin) + resp.headers.add("Access-Control-Allow-Origin", origin) elif any(domain.match(origin) for domain in domains_re_compiled): - resp.headers.add('Access-Control-Allow-Origin', origin) + resp.headers.add("Access-Control-Allow-Origin", origin) else: - resp.headers.add('Access-Control-Allow-Origin', '') - resp.headers.add('Access-Control-Allow-Headers', ', '.join(headers)) - resp.headers.add('Access-Control-Expose-Headers', - ', '.join(expose_headers)) - resp.headers.add('Access-Control-Allow-Methods', methods) - resp.headers.add('Access-Control-Max-Age', config.X_MAX_AGE) + resp.headers.add("Access-Control-Allow-Origin", "") + resp.headers.add("Access-Control-Allow-Headers", ", ".join(headers)) + resp.headers.add("Access-Control-Expose-Headers", ", ".join(expose_headers)) + resp.headers.add("Access-Control-Allow-Methods", methods) + resp.headers.add("Access-Control-Max-Age", config.X_MAX_AGE) if allow_credentials: - resp.headers.add('Access-Control-Allow-Credentials', "true") + resp.headers.add("Access-Control-Allow-Credentials", "true") # Rate-Limiting limit = get_rate_limit() if limit and limit.send_x_headers: - resp.headers.add('X-RateLimit-Remaining', str(limit.remaining)) - resp.headers.add('X-RateLimit-Limit', str(limit.limit)) - resp.headers.add('X-RateLimit-Reset', str(limit.reset)) + resp.headers.add("X-RateLimit-Remaining", str(limit.remaining)) + resp.headers.add("X-RateLimit-Limit", str(limit.limit)) + resp.headers.add("X-RateLimit-Reset", str(limit.reset)) return resp @@ -260,19 +267,21 @@ def _best_mime(): """ supported = [] renders = {} - for renderer_cls in app.config.get('RENDERERS'): + for renderer_cls in app.config.get("RENDERERS"): renderer = import_from_string(renderer_cls) for mime_type in renderer.mime: supported.append(mime_type) renders[mime_type] = renderer if len(supported) == 0: - abort(500, description=debug_error_message( - 'Configuration error: no supported mime types') + abort( + 500, + description=debug_error_message( + "Configuration error: no supported mime types" + ), ) - best_match = request.accept_mimetypes.best_match(supported) or \ - supported[0] + best_match = request.accept_mimetypes.best_match(supported) or supported[0] return best_match, renders[best_match] @@ -281,18 +290,19 @@ class Renderer(object): attr and have `.render()` method implemented. """ + mime = tuple() def render(self, data): - raise NotImplementedError('Renderer .render() method is not ' - 'implemented') + raise NotImplementedError("Renderer .render() method is not " "implemented") class JSONRenderer(Renderer): """ JSON renderer class based on `simplejson` package. """ - mime = ('application/json',) + + mime = ("application/json",) def render(self, data): """ JSON render function @@ -309,19 +319,23 @@ def render(self, data): set_indent = None # make pretty prints available - if 'GET' in request.method and 'pretty' in request.args: + if "GET" in request.method and "pretty" in request.args: set_indent = 4 - return json.dumps(data, indent=set_indent, - cls=app.data.json_encoder_class, - sort_keys=config.JSON_SORT_KEYS) + return json.dumps( + data, + indent=set_indent, + cls=app.data.json_encoder_class, + sort_keys=config.JSON_SORT_KEYS, + ) class XMLRenderer(Renderer): """ XML renderer class. """ - mime = ('application/xml', 'text/xml', 'application/x-xml',) - tag = 'XML' + + mime = ("application/xml", "text/xml", "application/x-xml") + tag = "XML" def render(self, data): """ XML render function. @@ -343,7 +357,7 @@ def render(self, data): if isinstance(data, list): data = {config.ITEMS: data} - xml = '' + xml = "" if data: xml += self.xml_root_open(data) xml += self.xml_add_links(data) @@ -370,13 +384,13 @@ def xml_root_open(cls, data): .. versionadded:: 0.0.3 """ links = data.get(config.LINKS) - href = title = '' - if links and 'self' in links: - self_ = links.pop('self') - href = ' href="%s" ' % utils.escape(self_['href']) - if 'title' in self_: - title = ' title="%s" ' % self_['title'] - return '' % (href, title) + href = title = "" + if links and "self" in links: + self_ = links.pop("self") + href = ' href="%s" ' % utils.escape(self_["href"]) + if "title" in self_: + title = ' title="%s" ' % self_["title"] + return "" % (href, title) @classmethod def xml_add_meta(cls, data): @@ -389,14 +403,14 @@ def xml_add_meta(cls, data): .. versionadded:: 0.4 """ - xml = '' + xml = "" meta = [] if data.get(config.META): ordered_meta = OrderedDict(sorted(data[config.META].items())) for name, value in ordered_meta.items(): - meta.append('<%s>%d' % (name, value, name)) + meta.append("<%s>%d" % (name, value, name)) if meta: - xml = '<%s>%s' % (config.META, ''.join(meta), config.META) + xml = "<%s>%s" % (config.META, "".join(meta), config.META) return xml @classmethod @@ -415,18 +429,20 @@ def xml_add_links(cls, data): .. versionadded:: 0.0.3 """ - xml = '' + xml = "" chunk = '' links = data.pop(config.LINKS, {}) ordered_links = OrderedDict(sorted(links.items())) for rel, link in ordered_links.items(): if isinstance(link, list): - xml += ''.join([chunk % (rel, utils.escape(d['href']), - utils.escape(d['title'])) - for d in link]) + xml += "".join( + [ + chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) + for d in link + ] + ) else: - xml += ''.join(chunk % (rel, utils.escape(link['href']), - link['title'])) + xml += "".join(chunk % (rel, utils.escape(link["href"]), link["title"])) return xml @classmethod @@ -440,7 +456,7 @@ def xml_add_items(cls, data): .. versionadded:: 0.0.3 """ try: - xml = ''.join([cls.xml_item(item) for item in data[config.ITEMS]]) + xml = "".join([cls.xml_item(item) for item in data[config.ITEMS]]) except: xml = cls.xml_dict(data) return xml @@ -465,7 +481,7 @@ def xml_root_close(cls): .. versionadded:: 0.0.3 """ - return '' + return "" @classmethod def xml_dict(cls, data): @@ -481,7 +497,7 @@ def xml_dict(cls, data): .. versionadded:: 0.0.3 """ - xml = '' + xml = "" ordered_items = OrderedDict(sorted(data.items())) for k, v in ordered_items.items(): if isinstance(v, datetime.datetime): diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index c78c242fc..8f4bb3e59 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -9,10 +9,17 @@ from datetime import datetime, timedelta from pymongo import MongoClient from bson import ObjectId -from eve.tests.test_settings import MONGO_PASSWORD, MONGO_USERNAME, \ - MONGO_DBNAME, DOMAIN, MONGO_HOST, MONGO_PORT +from eve.tests.test_settings import ( + MONGO_PASSWORD, + MONGO_USERNAME, + MONGO_DBNAME, + DOMAIN, + MONGO_HOST, + MONGO_PORT, +) from eve import ISSUES, ETAG from eve.utils import date_to_str + try: from urlparse import parse_qs, urlparse except ImportError: @@ -27,6 +34,7 @@ class ValueStack(object): keep track by hand of the applications created in order to close their database connections. This descriptor helps with it. """ + def __init__(self, on_delete): """ :param on_delete: Action to execute when the attribute is deleted @@ -50,9 +58,9 @@ def close_pymongo_connection(app): """ Close the pymongo connection in an eve/flask app """ - if 'pymongo' not in app.extensions: + if "pymongo" not in app.extensions: return - del app.extensions['pymongo'] + del app.extensions["pymongo"] del app.media @@ -61,6 +69,7 @@ class TestMinimal(unittest.TestCase): based on Eve by subclassing this class and provide proper settings using :func:`setUp()` """ + app = ValueStack(close_pymongo_connection) def setUp(self, settings_file=None, url_converters=None): @@ -72,20 +81,18 @@ def setUp(self, settings_file=None, url_converters=None): self.this_directory = os.path.dirname(os.path.realpath(__file__)) if settings_file is None: # Load the settings file, using a robust path - settings_file = os.path.join(self.this_directory, - 'test_settings.py') + settings_file = os.path.join(self.this_directory, "test_settings.py") self.connection = None self.known_resource_count = 101 self.setupDB() self.settings_file = settings_file - self.app = eve.Eve(settings=self.settings_file, - url_converters=url_converters) + self.app = eve.Eve(settings=self.settings_file, url_converters=url_converters) self.test_client = self.app.test_client() - self.domain = self.app.config['DOMAIN'] + self.domain = self.app.config["DOMAIN"] def tearDown(self): del self.app @@ -112,35 +119,35 @@ def assert404(self, status): def assert422(self, status): self.assertEqual(status, 422) - def get(self, resource, query='', item=None): + def get(self, resource, query="", item=None): if resource in self.domain: - resource = self.domain[resource]['url'] + resource = self.domain[resource]["url"] if item: - request = '/%s/%s%s' % (resource, item, query) + request = "/%s/%s%s" % (resource, item, query) else: - request = '/%s%s' % (resource, query) + request = "/%s%s" % (resource, query) r = self.test_client.get(request) return self.parse_response(r) - def post(self, url, data, headers=None, content_type='application/json'): + def post(self, url, data, headers=None, content_type="application/json"): if headers is None: headers = [] - headers.append(('Content-Type', content_type)) + headers.append(("Content-Type", content_type)) r = self.test_client.post(url, data=json.dumps(data), headers=headers) return self.parse_response(r) def put(self, url, data, headers=None): if headers is None: headers = [] - headers.append(('Content-Type', 'application/json')) + headers.append(("Content-Type", "application/json")) r = self.test_client.put(url, data=json.dumps(data), headers=headers) return self.parse_response(r) def patch(self, url, data, headers=None): if headers is None: headers = [] - headers.append(('Content-Type', 'application/json')) + headers.append(("Content-Type", "application/json")) r = self.test_client.patch(url, data=json.dumps(data), headers=headers) return self.parse_response(r) @@ -156,8 +163,7 @@ def parse_response(self, r): return v, r.status_code def assertValidationErrorStatus(self, status): - self.assertEqual(status, - self.app.config.get('VALIDATION_ERROR_STATUS')) + self.assertEqual(status, self.app.config.get("VALIDATION_ERROR_STATUS")) def assertValidationError(self, response, matches): self.assertTrue(eve.STATUS in response) @@ -175,130 +181,139 @@ def assertExpires(self, resource): # it with Expires r = self.test_client.get(resource) - expires = r.headers.get('Expires') + expires = r.headers.get("Expires") self.assertTrue(expires is not None) def assertCacheControl(self, resource): r = self.test_client.get(resource) - cache_control = r.headers.get('Cache-Control') + cache_control = r.headers.get("Cache-Control") self.assertTrue(cache_control is not None) - self.assertEqual(cache_control, - self.domain[self.known_resource]['cache_control']) + self.assertEqual( + cache_control, self.domain[self.known_resource]["cache_control"] + ) def assertIfModifiedSince(self, resource): r = self.test_client.get(resource) - last_modified = r.headers.get('Last-Modified') + last_modified = r.headers.get("Last-Modified") self.assertTrue(last_modified is not None) - r = self.test_client.get(resource, headers=[('If-Modified-Since', - last_modified)]) + r = self.test_client.get( + resource, headers=[("If-Modified-Since", last_modified)] + ) self.assert304(r.status_code) self.assertTrue(not r.get_data()) def assertItem(self, item, resource): self.assertEqual(type(item), dict) - updated_on = item.get(self.app.config['LAST_UPDATED']) + updated_on = item.get(self.app.config["LAST_UPDATED"]) self.assertTrue(updated_on is not None) try: - datetime.strptime(updated_on, self.app.config['DATE_FORMAT']) + datetime.strptime(updated_on, self.app.config["DATE_FORMAT"]) except Exception as e: - self.fail('Cannot convert field "%s" to datetime: %s' % - (self.app.config['LAST_UPDATED'], e)) + self.fail( + 'Cannot convert field "%s" to datetime: %s' + % (self.app.config["LAST_UPDATED"], e) + ) - created_on = item.get(self.app.config['DATE_CREATED']) + created_on = item.get(self.app.config["DATE_CREATED"]) self.assertTrue(updated_on is not None) try: - datetime.strptime(created_on, self.app.config['DATE_FORMAT']) + datetime.strptime(created_on, self.app.config["DATE_FORMAT"]) except Exception as e: - self.fail('Cannot convert field "%s" to datetime: %s' % - (self.app.config['DATE_CREATED'], e)) + self.fail( + 'Cannot convert field "%s" to datetime: %s' + % (self.app.config["DATE_CREATED"], e) + ) - link = item.get('_links') - _id = item.get(self.domain[resource]['id_field']) + link = item.get("_links") + _id = item.get(self.domain[resource]["id_field"]) self.assertItemLink(link, _id) def assertPagination(self, response, page, total, max_results): - p_key, mr_key = self.app.config['QUERY_PAGE'], \ - self.app.config['QUERY_MAX_RESULTS'] - self.assertTrue(self.app.config['META'] in response) - meta = response.get(self.app.config['META']) + p_key, mr_key = ( + self.app.config["QUERY_PAGE"], + self.app.config["QUERY_MAX_RESULTS"], + ) + self.assertTrue(self.app.config["META"] in response) + meta = response.get(self.app.config["META"]) self.assertTrue(p_key in meta) self.assertTrue(mr_key in meta) - self.assertTrue('total' in meta) + self.assertTrue("total" in meta) self.assertEqual(meta[p_key], page) self.assertEqual(meta[mr_key], max_results) - self.assertEqual(meta['total'], total) + self.assertEqual(meta["total"], total) def assertHomeLink(self, links): - self.assertTrue('parent' in links) - link = links['parent'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - self.assertEqual('home', link['title']) - self.assertEqual("/", link['href']) + self.assertTrue("parent" in links) + link = links["parent"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + self.assertEqual("home", link["title"]) + self.assertEqual("/", link["href"]) def assertResourceLink(self, links, resource): - self.assertTrue('self' in links) - link = links['self'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - url = self.domain[resource]['url'] - self.assertEqual(url, link['title']) - self.assertEqual("%s" % url, link['href']) + self.assertTrue("self" in links) + link = links["self"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + url = self.domain[resource]["url"] + self.assertEqual(url, link["title"]) + self.assertEqual("%s" % url, link["href"]) def assertCollectionLink(self, links, resource): - self.assertTrue('collection' in links) - link = links['collection'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - url = self.domain[resource]['url'] - self.assertEqual(url, link['title']) - self.assertEqual("%s" % url, link['href']) + self.assertTrue("collection" in links) + link = links["collection"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + url = self.domain[resource]["url"] + self.assertEqual(url, link["title"]) + self.assertEqual("%s" % url, link["href"]) def assertNextLink(self, links, page): - self.assertTrue('next' in links) - link = links['next'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - self.assertEqual('next page', link['title']) - self.assertTrue("%s=%d" % (self.app.config['QUERY_PAGE'], page) - in link['href']) + self.assertTrue("next" in links) + link = links["next"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + self.assertEqual("next page", link["title"]) + self.assertTrue("%s=%d" % (self.app.config["QUERY_PAGE"], page) in link["href"]) def assertPrevLink(self, links, page): - self.assertTrue('prev' in links) - link = links['prev'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - self.assertEqual('previous page', link['title']) + self.assertTrue("prev" in links) + link = links["prev"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + self.assertEqual("previous page", link["title"]) if page > 1: - self.assertTrue("%s=%d" % (self.app.config['QUERY_PAGE'], page) - in link['href']) + self.assertTrue( + "%s=%d" % (self.app.config["QUERY_PAGE"], page) in link["href"] + ) def assertItemLink(self, links, item_id): - self.assertTrue('self' in links) - link = links['self'] + self.assertTrue("self" in links) + link = links["self"] # TODO we are too deep here to get a hold of the due title. Should fix. - self.assertTrue('title' in link) - self.assertTrue('href' in link) - self.assertTrue('/%s' % item_id in link['href']) + self.assertTrue("title" in link) + self.assertTrue("href" in link) + self.assertTrue("/%s" % item_id in link["href"]) def assertLastLink(self, links, page): if page: - self.assertTrue('last' in links) - link = links['last'] - self.assertTrue('title' in link) - self.assertTrue('href' in link) - self.assertEqual('last page', link['title']) - self.assertTrue("%s=%d" % (self.app.config['QUERY_PAGE'], page) - in link['href']) + self.assertTrue("last" in links) + link = links["last"] + self.assertTrue("title" in link) + self.assertTrue("href" in link) + self.assertEqual("last page", link["title"]) + self.assertTrue( + "%s=%d" % (self.app.config["QUERY_PAGE"], page) in link["href"] + ) else: - self.assertTrue('last' not in links) + self.assertTrue("last" not in links) def assertCustomParams(self, link, params): - self.assertTrue('href' in link) - url_params = parse_qs(urlparse(link['href']).query) + self.assertTrue("href" in link) + url_params = parse_qs(urlparse(link["href"]).query) for param, values in params.lists(): self.assertTrue(param in url_params) for value in values: @@ -333,9 +348,10 @@ def setupDB(self): self.connection.drop_database(MONGO_DBNAME) if MONGO_USERNAME: db = self.connection[MONGO_DBNAME] - db.command('dropUser', MONGO_USERNAME) - db.command('createUser', MONGO_USERNAME, pwd=MONGO_PASSWORD, - roles=['dbAdmin']) + db.command("dropUser", MONGO_USERNAME) + db.command( + "createUser", MONGO_USERNAME, pwd=MONGO_PASSWORD, roles=["dbAdmin"] + ) self.bulk_insert() def bulk_insert(self): @@ -348,128 +364,127 @@ def dropDB(self): class TestBase(TestMinimal): - def setUp(self, url_converters=None): super(TestBase, self).setUp(url_converters=url_converters) - self.disabled_bulk = 'disabled_bulk' - self.disabled_bulk_url = ('/%s' % - self.domain[self.disabled_bulk]['url']) - - self.known_resource = 'contacts' - self.known_resource_url = ('/%s' % - self.domain[self.known_resource]['url']) - self.empty_resource = 'empty' - self.empty_resource_url = '/%s' % self.empty_resource - - self.unknown_resource = 'unknown' - self.unknown_resource_url = '/%s' % self.unknown_resource - self.unknown_item_id = '4f46445fc88e201858000000' - self.unknown_item_name = 'unknown' - - self.unknown_item_id_url = ('/%s/%s' % - (self.domain[self.known_resource]['url'], - self.unknown_item_id)) - self.unknown_item_name_url = ('/%s/%s' % - (self.domain[self.known_resource]['url'], - self.unknown_item_name)) - - self.readonly_resource = 'payments' - self.readonly_resource_url = ( - '/%s' % self.domain[self.readonly_resource]['url']) - - self.different_resource = 'users' - self.different_resource_url = ('/%s' % - self.domain[ - self.different_resource]['url']) - - self.different_resource_exclude = 'contacts_hide_born' + self.disabled_bulk = "disabled_bulk" + self.disabled_bulk_url = "/%s" % self.domain[self.disabled_bulk]["url"] + + self.known_resource = "contacts" + self.known_resource_url = "/%s" % self.domain[self.known_resource]["url"] + self.empty_resource = "empty" + self.empty_resource_url = "/%s" % self.empty_resource + + self.unknown_resource = "unknown" + self.unknown_resource_url = "/%s" % self.unknown_resource + self.unknown_item_id = "4f46445fc88e201858000000" + self.unknown_item_name = "unknown" + + self.unknown_item_id_url = "/%s/%s" % ( + self.domain[self.known_resource]["url"], + self.unknown_item_id, + ) + self.unknown_item_name_url = "/%s/%s" % ( + self.domain[self.known_resource]["url"], + self.unknown_item_name, + ) + + self.readonly_resource = "payments" + self.readonly_resource_url = "/%s" % self.domain[self.readonly_resource]["url"] + + self.different_resource = "users" + self.different_resource_url = ( + "/%s" % self.domain[self.different_resource]["url"] + ) + + self.different_resource_exclude = "contacts_hide_born" self.different_resource_exclude_url = ( - '/%s' % self.domain[self.different_resource_exclude]['url']) + "/%s" % self.domain[self.different_resource_exclude]["url"] + ) - self.resource_exclude_media = 'contacts_hide_media' + self.resource_exclude_media = "contacts_hide_media" self.resource_exclude_media_url = ( - '/%s' % self.domain[self.resource_exclude_media]['url']) + "/%s" % self.domain[self.resource_exclude_media]["url"] + ) - response, _ = self.get('contacts', '?max_results=2') + response, _ = self.get("contacts", "?max_results=2") contact = self.response_item(response) self.item = contact - self.item_id = contact[self.domain['contacts']['id_field']] - self.item_name = contact['ref'] - self.item_tid = contact['tid'] + self.item_id = contact[self.domain["contacts"]["id_field"]] + self.item_name = contact["ref"] + self.item_tid = contact["tid"] self.item_etag = contact[ETAG] - self.item_ref = contact['ref'] - self.item_id_url = ('/%s/%s' % - (self.domain[self.known_resource]['url'], - self.item_id)) - self.item_name_url = ('/%s/%s' % - (self.domain[self.known_resource]['url'], - self.item_name)) - self.alt_ref = self.response_item(response, 1)['ref'] - - response, _ = self.get('payments', '?max_results=1') - self.readonly_id = self.response_item(response)['_id'] - self.readonly_id_url = ('%s/%s' % (self.readonly_resource_url, - self.readonly_id)) - - response, _ = self.get('users') + self.item_ref = contact["ref"] + self.item_id_url = "/%s/%s" % ( + self.domain[self.known_resource]["url"], + self.item_id, + ) + self.item_name_url = "/%s/%s" % ( + self.domain[self.known_resource]["url"], + self.item_name, + ) + self.alt_ref = self.response_item(response, 1)["ref"] + + response, _ = self.get("payments", "?max_results=1") + self.readonly_id = self.response_item(response)["_id"] + self.readonly_id_url = "%s/%s" % (self.readonly_resource_url, self.readonly_id) + + response, _ = self.get("users") user = self.response_item(response) - self.user_id = user[self.domain['users']['id_field']] - self.user_username = user['username'] - self.user_name = user['ref'] + self.user_id = user[self.domain["users"]["id_field"]] + self.user_username = user["username"] + self.user_name = user["ref"] self.user_etag = user[ETAG] - self.user_id_url = ('/%s/%s' % - (self.domain[self.different_resource]['url'], - self.user_id)) - self.user_username_url = ( - '/%s/%s' % (self.domain[self.different_resource]['url'], - self.user_username) + self.user_id_url = "/%s/%s" % ( + self.domain[self.different_resource]["url"], + self.user_id, + ) + self.user_username_url = "/%s/%s" % ( + self.domain[self.different_resource]["url"], + self.user_username, ) - response, _ = self.get('invoices') + response, _ = self.get("invoices") invoice = self.response_item(response) - self.invoice_id = invoice[self.domain['invoices']['id_field']] + self.invoice_id = invoice[self.domain["invoices"]["id_field"]] self.invoice_etag = invoice[ETAG] - self.invoice_id_url = ('/%s/%s' % - (self.domain['invoices']['url'], - self.invoice_id)) + self.invoice_id_url = "/%s/%s" % ( + self.domain["invoices"]["url"], + self.invoice_id, + ) self.epoch = date_to_str(datetime(1970, 1, 1)) - self.products = 'products' - self.products_url = ('/%s' % - self.domain[self.products]['url']) + self.products = "products" + self.products_url = "/%s" % self.domain[self.products]["url"] - self.child_products = 'child_products' - self.child_products_url = ('/%s' % - self.domain[self.child_products]['url']) + self.child_products = "child_products" + self.child_products_url = "/%s" % self.domain[self.child_products]["url"] def response_item(self, response, i=0): - if self.app.config['HATEOAS']: - return response['_items'][i] + if self.app.config["HATEOAS"]: + return response["_items"][i] else: return response[i] def random_contacts(self, num, standard_date_fields=True): - schema = DOMAIN['contacts']['schema'] + schema = DOMAIN["contacts"]["schema"] contacts = [] for i in range(num): dt = datetime.utcnow().replace(microsecond=0) contact = { - 'ref': self.random_string(schema['ref']['maxlength']), - 'prog': i, - 'role': random.choice(schema['role']['allowed']), - 'rows': self.random_rows(random.randint(0, 5)), - 'alist': self.random_list(random.randint(0, 5)), - 'location': { - 'address': 'address ' + self.random_string(5), - 'city': 'city ' + self.random_string(3), + "ref": self.random_string(schema["ref"]["maxlength"]), + "prog": i, + "role": random.choice(schema["role"]["allowed"]), + "rows": self.random_rows(random.randint(0, 5)), + "alist": self.random_list(random.randint(0, 5)), + "location": { + "address": "address " + self.random_string(5), + "city": "city " + self.random_string(3), }, - 'born': datetime.today() + timedelta( - days=random.randint(-10, 10)), - - 'tid': ObjectId(), - 'read_only_field': schema['read_only_field']['default'] + "born": datetime.today() + timedelta(days=random.randint(-10, 10)), + "tid": ObjectId(), + "read_only_field": schema["read_only_field"]["default"], } if standard_date_fields: contact[eve.LAST_UPDATED] = dt @@ -481,7 +496,7 @@ def random_contacts(self, num, standard_date_fields=True): def random_users(self, num): users = self.random_contacts(num) for user in users: - user['username'] = self.random_string(10) + user["username"] = self.random_string(10) return users def random_payments(self, num): @@ -489,8 +504,8 @@ def random_payments(self, num): for i in range(num): dt = datetime.utcnow().replace(microsecond=0) payment = { - 'a_string': self.random_string(10), - 'a_number': i, + "a_string": self.random_string(10), + "a_number": i, eve.LAST_UPDATED: dt, eve.DATE_CREATED: dt, } @@ -502,7 +517,7 @@ def random_invoices(self, num): for _ in range(num): dt = datetime.utcnow().replace(microsecond=0) invoice = { - 'inv_number': self.random_string(10), + "inv_number": self.random_string(10), eve.LAST_UPDATED: dt, eve.DATE_CREATED: dt, } @@ -510,36 +525,38 @@ def random_invoices(self, num): return invoices def random_products(self, num): - schema = DOMAIN['products']['schema'] + schema = DOMAIN["products"]["schema"] products = [] for _ in range(num): products.append( { - 'sku': self.random_string(schema['sku']['maxlength']), - 'title': ("Hypercube " + self.random_string(2) + - str(random.randint(100, 1000))) + "sku": self.random_string(schema["sku"]["maxlength"]), + "title": ( + "Hypercube " + + self.random_string(2) + + str(random.randint(100, 1000)) + ), } ) return products def random_string(self, num): - return (''.join(random.choice(string.ascii_uppercase) - for x in range(num))) + return "".join(random.choice(string.ascii_uppercase) for x in range(num)) def random_list(self, num): alist = [] for i in range(num): - alist.append(['string' + str(i), random.randint(1000, 9999)]) + alist.append(["string" + str(i), random.randint(1000, 9999)]) return alist def random_rows(self, num): - schema = DOMAIN['contacts']['schema']['rows']['schema']['schema'] + schema = DOMAIN["contacts"]["schema"]["rows"]["schema"]["schema"] rows = [] for _ in range(num): rows.append( { - 'sku': self.random_string(schema['sku']['maxlength']), - 'price': random.randint(100, 1000), + "sku": self.random_string(schema["sku"]["maxlength"]), + "price": random.randint(100, 1000), } ) return rows @@ -549,8 +566,8 @@ def random_internal_transactions(self, num): for i in range(num): dt = datetime.utcnow().replace(microsecond=0) transaction = { - 'internal_string': self.random_string(10), - 'internal_number': i, + "internal_string": self.random_string(10), + "internal_number": i, eve.LAST_UPDATED: dt, eve.DATE_CREATED: dt, } @@ -559,20 +576,18 @@ def random_internal_transactions(self, num): def generate_products(self): products = self.random_products(10) - skus = [product['sku'] for product in products] + skus = [product["sku"] for product in products] for counter, sku in enumerate(skus[5:], 0): - products[counter]['parent_product'] = sku + products[counter]["parent_product"] = sku return products def bulk_insert(self): _db = self.connection[MONGO_DBNAME] - _db.contacts.insert_many(self.random_contacts( - self.known_resource_count)) + _db.contacts.insert_many(self.random_contacts(self.known_resource_count)) _db.contacts.insert_many(self.random_users(2)) _db.payments.insert_many(self.random_payments(10)) _db.invoices.insert_many(self.random_invoices(1)) - _db.internal_transactions.insert_many( - self.random_internal_transactions(4)) + _db.internal_transactions.insert_many(self.random_internal_transactions(4)) products = self.generate_products() _db.products.insert_many(products) self.connection.close() diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 35681f0f6..8600b7226 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -13,13 +13,16 @@ class ValidBasicAuth(BasicAuth): def __init__(self): - self.request_auth_value = 'admin' + self.request_auth_value = "admin" super(ValidBasicAuth, self).__init__() def check_auth(self, username, password, allowed_roles, resource, method): self.set_request_auth_value(self.request_auth_value) - return username in ('admin', 'alt') and password == 'secret' and \ - ('admin' in allowed_roles if allowed_roles else True) + return ( + username in ("admin", "alt") + and password == "secret" + and ("admin" in allowed_roles if allowed_roles else True) + ) class BadBasicAuth(BasicAuth): @@ -28,8 +31,9 @@ class BadBasicAuth(BasicAuth): class ValidTokenAuth(TokenAuth): def check_auth(self, token, allowed_roles, resource, method): - return token == 'test_token' and ('admin' in allowed_roles if - allowed_roles else True) + return token == "test_token" and ( + "admin" in allowed_roles if allowed_roles else True + ) class BadTokenAuth(TokenAuth): @@ -37,11 +41,15 @@ class BadTokenAuth(TokenAuth): class ValidHMACAuth(HMACAuth): - def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, - resource, method): + def check_auth( + self, userid, hmac_hash, headers, data, allowed_roles, resource, method + ): self.set_request_auth_value(userid) - return userid == 'admin' and hmac_hash == 'secret' and \ - ('admin' in allowed_roles if allowed_roles else True) + return ( + userid == "admin" + and hmac_hash == "secret" + and ("admin" in allowed_roles if allowed_roles else True) + ) class BadHMACAuth(HMACAuth): @@ -49,34 +57,36 @@ class BadHMACAuth(HMACAuth): class TestBasicAuth(TestBase): - def setUp(self): super(TestBasicAuth, self).setUp() self.app = Eve(settings=self.settings_file, auth=ValidBasicAuth) self.test_client = self.app.test_client() - self.content_type = ('Content-Type', 'application/json') - self.valid_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0'), - self.content_type] - self.invalid_auth = [('Authorization', 'Basic IDontThinkSo'), - self.content_type] - self.valid_media_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0'), - ('Content-Type', 'multipart/form-data')] + self.content_type = ("Content-Type", "application/json") + self.valid_auth = [ + ("Authorization", "Basic YWRtaW46c2VjcmV0"), + self.content_type, + ] + self.invalid_auth = [("Authorization", "Basic IDontThinkSo"), self.content_type] + self.valid_media_auth = [ + ("Authorization", "Basic YWRtaW46c2VjcmV0"), + ("Content-Type", "multipart/form-data"), + ] self.setUpRoles() self.app.set_defaults() def setUpRoles(self): - for _, schema in self.app.config['DOMAIN'].items(): - schema['allowed_roles'] = ['admin'] - schema['allowed_read_roles'] = ['reader'] - schema['allowed_item_roles'] = ['admin'] - schema['allowed_item_read_roles'] = ['reader'] - schema['allowed_item_write_roles'] = ['editor'] + for _, schema in self.app.config["DOMAIN"].items(): + schema["allowed_roles"] = ["admin"] + schema["allowed_read_roles"] = ["reader"] + schema["allowed_item_roles"] = ["admin"] + schema["allowed_item_read_roles"] = ["reader"] + schema["allowed_item_write_roles"] = ["editor"] def test_custom_auth(self): self.assertTrue(isinstance(self.app.auth, ValidBasicAuth)) def test_restricted_home_access(self): - r = self.test_client.get('/') + r = self.test_client.get("/") self.assert401(r.status_code) def test_restricted_resource_access(self): @@ -96,71 +106,72 @@ def test_restricted_item_access(self): self.assert401(r.status_code) def test_authorized_home_access(self): - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) self.assert200(r.status_code) def test_authorized_resource_access(self): - r = self.test_client.get(self.known_resource_url, - headers=self.valid_auth) + r = self.test_client.get(self.known_resource_url, headers=self.valid_auth) self.assert200(r.status_code) - r = self.test_client.post(self.known_resource_url, - data=json.dumps({"k": "value"}), - headers=self.valid_auth) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps({"k": "value"}), + headers=self.valid_auth, + ) self.assertValidationErrorStatus(r.status_code) - r = self.test_client.delete(self.known_resource_url, - headers=self.valid_auth) + r = self.test_client.delete(self.known_resource_url, headers=self.valid_auth) self.assert204(r.status_code) def test_authorized_item_access(self): r = self.test_client.get(self.item_id_url, headers=self.valid_auth) self.assert200(r.status_code) - r = self.test_client.patch(self.item_id_url, - data=json.dumps({"k": "value"}), - headers=self.valid_auth) + r = self.test_client.patch( + self.item_id_url, data=json.dumps({"k": "value"}), headers=self.valid_auth + ) self.assert428(r.status_code) r = self.test_client.delete(self.item_id_url, headers=self.valid_auth) self.assert428(r.status_code) def test_authorized_media_access(self): - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False - self.app.config['RETURN_MEDIA_AS_URL'] = True - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False + self.app.config["RETURN_MEDIA_AS_URL"] = True + self.app.config["BANDWIDTH_SAVER"] = False self.app._init_media_endpoint() - clean = b'my new file contents' - test_field, test_value = 'ref', "9234567890123456789054321" - data = {'media': (BytesIO(clean), 'test.txt'), test_field: test_value} - r, s = self.parse_response(self.test_client.post( - self.known_resource_url, data=data, headers=self.valid_media_auth)) + clean = b"my new file contents" + test_field, test_value = "ref", "9234567890123456789054321" + data = {"media": (BytesIO(clean), "test.txt"), test_field: test_value} + r, s = self.parse_response( + self.test_client.post( + self.known_resource_url, data=data, headers=self.valid_media_auth + ) + ) self.assert201(s) - file_url = r['media'] + file_url = r["media"] r = self.test_client.get(file_url, headers=self.invalid_auth) self.assert401(r.status_code) r = self.test_client.get(file_url, headers=self.valid_auth) self.assert200(r.status_code) def test_authorized_schema_access(self): - self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() - r = self.test_client.get('/schema/%s' % self.known_resource, - headers=self.valid_auth) + r = self.test_client.get( + "/schema/%s" % self.known_resource, headers=self.valid_auth + ) self.assert200(r.status_code) def test_unauthorized_home_access(self): - r = self.test_client.get('/', headers=self.invalid_auth) + r = self.test_client.get("/", headers=self.invalid_auth) self.assert401(r.status_code) def test_unauthorized_resource_access(self): - r = self.test_client.get(self.known_resource_url, - headers=self.invalid_auth) + r = self.test_client.get(self.known_resource_url, headers=self.invalid_auth) self.assert401(r.status_code) - r = self.test_client.post(self.known_resource_url, - headers=self.invalid_auth) + r = self.test_client.post(self.known_resource_url, headers=self.invalid_auth) self.assert401(r.status_code) - r = self.test_client.delete(self.known_resource_url, - headers=self.invalid_auth) + r = self.test_client.delete(self.known_resource_url, headers=self.invalid_auth) self.assert401(r.status_code) def test_unauthorized_item_access(self): @@ -168,70 +179,70 @@ def test_unauthorized_item_access(self): self.assert401(r.status_code) r = self.test_client.patch(self.item_id_url, headers=self.invalid_auth) self.assert401(r.status_code) - r = self.test_client.delete(self.item_id_url, - headers=self.invalid_auth) + r = self.test_client.delete(self.item_id_url, headers=self.invalid_auth) self.assert401(r.status_code) def test_unauthorized_schema_access(self): - self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() - r = self.test_client.get('/schema/%s' % self.known_resource, - headers=self.invalid_auth) + r = self.test_client.get( + "/schema/%s" % self.known_resource, headers=self.invalid_auth + ) self.assert401(r.status_code) def test_home_public_methods(self): - self.app.config['PUBLIC_METHODS'] = ['GET'] - r = self.test_client.get('/') + self.app.config["PUBLIC_METHODS"] = ["GET"] + r = self.test_client.get("/") self.assert200(r.status_code) self.test_restricted_resource_access() self.test_restricted_item_access() def test_public_methods_resource(self): - self.app.config['PUBLIC_METHODS'] = ['GET'] - domain = self.app.config['DOMAIN'] + self.app.config["PUBLIC_METHODS"] = ["GET"] + domain = self.app.config["DOMAIN"] for resource, settings in domain.items(): - del(settings['public_methods']) + del (settings["public_methods"]) self.app.set_defaults() - del(domain['peopleinvoices']) - del(domain['peoplerequiredinvoices']) - del(domain['peoplesearches']) - del(domain['internal_transactions']) - del(domain['child_products']) + del (domain["peopleinvoices"]) + del (domain["peoplerequiredinvoices"]) + del (domain["peoplesearches"]) + del (domain["internal_transactions"]) + del (domain["child_products"]) for resource in domain: - url = self.app.config['URLS'][resource] + url = self.app.config["URLS"][resource] r = self.test_client.get(url) self.assert200(r.status_code) - r = self.test_client.post(url, data={'key1': 'value1'}) + r = self.test_client.post(url, data={"key1": "value1"}) self.assert401or405(r.status_code) r = self.test_client.delete(url) self.assert401or405(r.status_code) self.test_restricted_item_access() def test_public_methods_but_locked_resource(self): - self.app.config['PUBLIC_METHODS'] = ['GET'] - domain = self.app.config['DOMAIN'] + self.app.config["PUBLIC_METHODS"] = ["GET"] + domain = self.app.config["DOMAIN"] for _, settings in domain.items(): - del(settings['public_methods']) + del (settings["public_methods"]) self.app.set_defaults() - domain[self.known_resource]['public_methods'] = [] + domain[self.known_resource]["public_methods"] = [] r = self.test_client.get(self.known_resource_url) self.assert401(r.status_code) def test_public_methods_but_locked_item(self): - self.app.config['PUBLIC_ITEM_METHODS'] = ['GET'] - domain = self.app.config['DOMAIN'] + self.app.config["PUBLIC_ITEM_METHODS"] = ["GET"] + domain = self.app.config["DOMAIN"] for _, settings in domain.items(): - del(settings['public_item_methods']) + del (settings["public_item_methods"]) self.app.set_defaults() - domain[self.known_resource]['public_item_methods'] = [] + domain[self.known_resource]["public_item_methods"] = [] r = self.test_client.get(self.item_id_url) self.assert401(r.status_code) def test_public_methods_item(self): - self.app.config['PUBLIC_ITEM_METHODS'] = ['GET'] - for _, settings in self.app.config['DOMAIN'].items(): - del(settings['public_item_methods']) + self.app.config["PUBLIC_ITEM_METHODS"] = ["GET"] + for _, settings in self.app.config["DOMAIN"].items(): + del (settings["public_item_methods"]) self.app.set_defaults() # we're happy with testing just one client endpoint, but for sake of # completeness we shold probably test item endpoints for every resource @@ -245,7 +256,7 @@ def test_public_methods_item(self): def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadBasicAuth) self.test_client = self.app.test_client() - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) # will fail because check_auth() is not implemented in the custom class self.assert500(r.status_code) @@ -261,30 +272,32 @@ def test_instanced_auth(self): auth = self.app.auth self.app = Eve(settings=self.settings_file, auth=auth) self.test_client = self.app.test_client() - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) self.assert200(r.status_code) def test_rfc2617_response(self): - r = self.test_client.get('/') + r = self.test_client.get("/") self.assert401(r.status_code) - self.assertTrue(('WWW-Authenticate', 'Basic realm="%s"' % - eve.__package__) in r.headers.to_wsgi_list()) + self.assertTrue( + ("WWW-Authenticate", 'Basic realm="%s"' % eve.__package__) + in r.headers.to_wsgi_list() + ) def test_allowed_roles_does_not_change(self): self.test_client.get(self.known_resource_url) - resource = self.app.config['DOMAIN'][self.known_resource] - self.assertEqual(resource['allowed_roles'], ['admin']) + resource = self.app.config["DOMAIN"][self.known_resource] + self.assertEqual(resource["allowed_roles"], ["admin"]) def test_allowed_item_roles_does_not_change(self): self.test_client.get(self.item_id_url) - resource = self.app.config['DOMAIN'][self.known_resource] - self.assertEqual(resource['allowed_item_roles'], ['admin']) + resource = self.app.config["DOMAIN"][self.known_resource] + self.assertEqual(resource["allowed_item_roles"], ["admin"]) def test_ALLOWED_ROLES_does_not_change(self): - self.app.config['ALLOWED_ROLES'] = ['admin'] - self.app.config['ALLOWED_READ_ROLES'] = ['reader'] - self.test_client.get('/') - self.assertEqual(self.app.config['ALLOWED_ROLES'], ['admin']) + self.app.config["ALLOWED_ROLES"] = ["admin"] + self.app.config["ALLOWED_READ_ROLES"] = ["reader"] + self.test_client.get("/") + self.assertEqual(self.app.config["ALLOWED_ROLES"], ["admin"]) class TestTokenAuth(TestBasicAuth): @@ -292,10 +305,14 @@ def setUp(self): super(TestTokenAuth, self).setUp() self.app = Eve(settings=self.settings_file, auth=ValidTokenAuth) self.test_client = self.app.test_client() - self.valid_auth = [('Authorization', 'Basic dGVzdF90b2tlbjo='), - self.content_type] - self.valid_media_auth = [('Authorization', 'Basic dGVzdF90b2tlbjo='), - ('Content-Type', 'multipart/form-data')] + self.valid_auth = [ + ("Authorization", "Basic dGVzdF90b2tlbjo="), + self.content_type, + ] + self.valid_media_auth = [ + ("Authorization", "Basic dGVzdF90b2tlbjo="), + ("Content-Type", "multipart/form-data"), + ] self.setUpRoles() def test_custom_auth(self): @@ -305,15 +322,16 @@ def test_custom_auth(self): class TestBearerTokenAuth(TestTokenAuth): def setUp(self): super(TestBearerTokenAuth, self).setUp() - self.valid_auth = [('Authorization', 'Token test_token'), - self.content_type] - self.valid_media_auth = [('Authorization', 'Token test_token'), - ('Content-Type', 'multipart/form-data')] + self.valid_auth = [("Authorization", "Token test_token"), self.content_type] + self.valid_media_auth = [ + ("Authorization", "Token test_token"), + ("Content-Type", "multipart/form-data"), + ] def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadTokenAuth) self.test_client = self.app.test_client() - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) # will fail because check_auth() is not implemented in the custom class self.assert500(r.status_code) @@ -321,15 +339,16 @@ def test_bad_auth_class(self): class TestCustomTokenAuth(TestTokenAuth): def setUp(self): super(TestCustomTokenAuth, self).setUp() - self.valid_auth = [('Authorization', 'Token test_token'), - self.content_type] - self.valid_media_auth = [('Authorization', 'Token test_token'), - ('Content-Type', 'multipart/form-data')] + self.valid_auth = [("Authorization", "Token test_token"), self.content_type] + self.valid_media_auth = [ + ("Authorization", "Token test_token"), + ("Content-Type", "multipart/form-data"), + ] def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadTokenAuth) self.test_client = self.app.test_client() - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) # will fail because check_auth() is not implemented in the custom class self.assert500(r.status_code) @@ -339,10 +358,11 @@ def setUp(self): super(TestHMACAuth, self).setUp() self.app = Eve(settings=self.settings_file, auth=ValidHMACAuth) self.test_client = self.app.test_client() - self.valid_auth = [('Authorization', 'admin:secret'), - self.content_type] - self.valid_media_auth = [('Authorization', 'admin:secret'), - ('Content-Type', 'multipart/form-data')] + self.valid_auth = [("Authorization", "admin:secret"), self.content_type] + self.valid_media_auth = [ + ("Authorization", "admin:secret"), + ("Content-Type", "multipart/form-data"), + ] self.setUpRoles() def test_custom_auth(self): @@ -351,31 +371,35 @@ def test_custom_auth(self): def test_bad_auth_class(self): self.app = Eve(settings=self.settings_file, auth=BadHMACAuth) self.test_client = self.app.test_client() - r = self.test_client.get('/', headers=self.valid_auth) + r = self.test_client.get("/", headers=self.valid_auth) # will fail because check_auth() is not implemented in the custom class self.assert500(r.status_code) def test_rfc2617_response(self): - r = self.test_client.get('/') + r = self.test_client.get("/") self.assert401(r.status_code) def test_post_resource_hmac_auth(self): # Test that user restricted access works with HMAC auth. - resource_def = self.app.config['DOMAIN']['restricted'] - resource_def['auth_field'] = 'username' - url = resource_def['url'] + resource_def = self.app.config["DOMAIN"]["restricted"] + resource_def["auth_field"] = "username" + url = resource_def["url"] data = {"ref": "0123456789123456789012345"} - r = self.app.test_client().post(url, data=json.dumps(data), - headers=self.valid_auth, - content_type='application/json') + r = self.app.test_client().post( + url, + data=json.dumps(data), + headers=self.valid_auth, + content_type="application/json", + ) # Verify that we can retrieve the same document r, status = self.parse_response( - self.app.test_client().get(url, headers=self.valid_auth)) + self.app.test_client().get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(len(r['_items']), 1) - self.assertEqual(r['_items'][0]['ref'], data['ref']) + self.assertEqual(len(r["_items"]), 1) + self.assertEqual(r["_items"][0]["ref"], data["ref"]) class TestResourceAuth(TestBase): @@ -384,12 +408,11 @@ def test_resource_only_auth(self): self.app = Eve(settings=self.settings_file) self.test_client = self.app.test_client() # explicit auth for just one resource - self.app.config['DOMAIN']['contacts']['authentication'] = \ - ValidBasicAuth() - self.app.config['DOMAIN']['empty']['authentication'] = ValidTokenAuth() + self.app.config["DOMAIN"]["contacts"]["authentication"] = ValidBasicAuth() + self.app.config["DOMAIN"]["empty"]["authentication"] = ValidTokenAuth() self.app.set_defaults() - basic_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0')] - token_auth = [('Authorization', 'Basic dGVzdF90b2tlbjo=')] + basic_auth = [("Authorization", "Basic YWRtaW46c2VjcmV0")] + token_auth = [("Authorization", "Basic dGVzdF90b2tlbjo=")] # 'contacts' endpoints are protected r = self.test_client.get(self.known_resource_url) @@ -398,10 +421,12 @@ def test_resource_only_auth(self): self.assert401(r.status_code) # both with BasicAuth. _, status = self.parse_response( - self.test_client.get(self.known_resource_url, headers=basic_auth)) + self.test_client.get(self.known_resource_url, headers=basic_auth) + ) self.assert200(status) _, status = self.parse_response( - self.test_client.get(self.item_id_url, headers=basic_auth)) + self.test_client.get(self.item_id_url, headers=basic_auth) + ) self.assert200(status) # 'empty' resource endpoint is also protected @@ -424,40 +449,41 @@ def setUp(self): # using this endpoint since it is a copy of 'contacts' with # no filter on the datasource - self.url = 'restricted' - self.resource = self.app.config['DOMAIN'][self.url] + self.url = "restricted" + self.resource = self.app.config["DOMAIN"][self.url] self.test_client = self.app.test_client() - self.valid_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0')] - self.invalid_auth = [('Authorization', 'Basic IDontThinkSo')] - self.field_name = 'auth_field' + self.valid_auth = [("Authorization", "Basic YWRtaW46c2VjcmV0")] + self.invalid_auth = [("Authorization", "Basic IDontThinkSo")] + self.field_name = "auth_field" self.data = json.dumps({"ref": "0123456789123456789012345"}) - for _, settings in self.app.config['DOMAIN'].items(): - settings[self.field_name] = 'username' + for _, settings in self.app.config["DOMAIN"].items(): + settings[self.field_name] = "username" - self.resource['public_methods'] = [] + self.resource["public_methods"] = [] def test_get(self): data, status = self.parse_response( - self.test_client.get(self.url, headers=self.valid_auth)) + self.test_client.get(self.url, headers=self.valid_auth) + ) self.assert200(status) # no data has been saved by user 'admin' yet, # so assert we get an empty result set back. - self.assertEqual(len(data['_items']), 0) + self.assertEqual(len(data["_items"]), 0) # Add a user belonging to `admin` new_user = self.random_contacts(1)[0] - new_user['username'] = 'admin' - _db = self.connection[self.app.config['MONGO_DBNAME']] + new_user["username"] = "admin" + _db = self.connection[self.app.config["MONGO_DBNAME"]] _db.contacts.insert_one(new_user) # Verify that we can retrieve it data2, status2 = self.parse_response( - self.test_client.get(self.url, - headers=self.valid_auth)) + self.test_client.get(self.url, headers=self.valid_auth) + ) self.assert200(status2) - self.assertEqual(len(data2['_items']), 1) + self.assertEqual(len(data2["_items"]), 1) def test_get_by_auth_field_criteria(self): """ If we attempt to retrieve an object by the same field @@ -468,20 +494,19 @@ def test_get_by_auth_field_criteria(self): a `client_filter` or url param. """ _, status = self.parse_response( - self.test_client.get(self.user_username_url, - headers=self.valid_auth)) + self.test_client.get(self.user_username_url, headers=self.valid_auth) + ) self.assert401(status) def test_get_by_auth_field_id(self): """ To test handling of ObjectIds """ # set auth_field to `_id` - self.domain['users'][self.field_name] = \ - self.domain['users']['id_field'] + self.domain["users"][self.field_name] = self.domain["users"]["id_field"] _, status = self.parse_response( - self.test_client.get(self.user_id_url, - headers=self.valid_auth)) + self.test_client.get(self.user_id_url, headers=self.valid_auth) + ) self.assert401(status) def test_filter_by_auth_field_id(self): @@ -489,74 +514,77 @@ def test_filter_by_auth_field_id(self): We need to make sure we *match* an object ID when it is the same """ - _id = ObjectId('deadbeefdeadbeefdeadbeef') - resource_def = self.app.config['DOMAIN']['users'] - resource_def['authentication'].request_auth_value = _id + _id = ObjectId("deadbeefdeadbeefdeadbeef") + resource_def = self.app.config["DOMAIN"]["users"] + resource_def["authentication"].request_auth_value = _id # set auth_field to `_id` - resource_def[self.field_name] = '_id' + resource_def[self.field_name] = "_id" # Retrieving a /different user/ by id returns 401 - user_url = '/users/' + user_url = "/users/" filter_by_id = 'where=_id==ObjectId("%s")' filter_query = filter_by_id % self.user_id _, status = self.parse_response( - self.test_client.get('%s?%s' % (user_url, filter_query), - headers=self.valid_auth)) + self.test_client.get( + "%s?%s" % (user_url, filter_query), headers=self.valid_auth + ) + ) self.assert401(status) # Create a user account belonging to admin new_user = self.random_contacts(1)[0] - new_user['_id'] = _id - new_user['username'] = 'admin' - _db = self.connection[self.app.config['MONGO_DBNAME']] + new_user["_id"] = _id + new_user["username"] = "admin" + _db = self.connection[self.app.config["MONGO_DBNAME"]] _db.contacts.insert_one(new_user) # Retrieving /the same/ user by id returns OK - filter_query_2 = filter_by_id % 'deadbeefdeadbeefdeadbeef' + filter_query_2 = filter_by_id % "deadbeefdeadbeefdeadbeef" data2, status2 = self.parse_response( - self.test_client.get('%s?%s' % (user_url, filter_query_2), - headers=self.valid_auth)) + self.test_client.get( + "%s?%s" % (user_url, filter_query_2), headers=self.valid_auth + ) + ) self.assert200(status2) - self.assertEqual(len(data2['_items']), 1) + self.assertEqual(len(data2["_items"]), 1) def test_collection_get_public(self): """ Test that if GET is in `public_methods` the `auth_field` criteria is overruled """ - self.resource['public_methods'].append('GET') - data, status = self.parse_response( - self.test_client.get(self.url)) # no auth + self.resource["public_methods"].append("GET") + data, status = self.parse_response(self.test_client.get(self.url)) # no auth self.assert200(status) # no data has been saved by user 'admin' yet, # but we should get all the other results back - self.assertEqual(len(data['_items']), 25) + self.assertEqual(len(data["_items"]), 25) def test_item_get_public(self): """ Test that if GET is in `public_item_methods` the `auth_field` criteria is overruled """ - self.resource['public_item_methods'].append('GET') + self.resource["public_item_methods"].append("GET") data, status = self.parse_response( - self.test_client.get(self.item_id_url, - headers=self.valid_auth)) + self.test_client.get(self.item_id_url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['_id'], self.item_id) + self.assertEqual(data["_id"], self.item_id) def test_post(self): _, status = self.post() self.assert201(status) data, status = self.parse_response( - self.test_client.get(self.url, - headers=self.valid_auth)) + self.test_client.get(self.url, headers=self.valid_auth) + ) self.assert200(status) # len of 1 as there are is only 1 doc saved by user def test_unique_to_user_on_post(self): # make the field unique to user, not globally. - self.resource['schema']['ref']['unique'] = False - self.resource['schema']['ref']['unique_to_user'] = True + self.resource["schema"]["ref"]["unique"] = False + self.resource["schema"]["ref"]["unique_to_user"] = True # first post as 'admin' is a success. _, status = self.post() @@ -566,21 +594,19 @@ def test_unique_to_user_on_post(self): _, status = self.post() self.assert422(status) - self.resource['authentication'].request_auth_value = 'alt' + self.resource["authentication"].request_auth_value = "alt" # first post as 'alt' succeeds as value is unique to this user. - alt_auth = [('Authorization', 'Basic YWx0OnNlY3JldA==')] - r = self.test_client.post(self.url, - data=self.data, - headers=alt_auth, - content_type='application/json') + alt_auth = [("Authorization", "Basic YWx0OnNlY3JldA==")] + r = self.test_client.post( + self.url, data=self.data, headers=alt_auth, content_type="application/json" + ) self.assert201(r.status_code) # second post as 'alt' fails since value is not unique to user anymore. - r = self.test_client.post(self.url, - data=self.data, - headers=alt_auth, - content_type='application/json') + r = self.test_client.post( + self.url, data=self.data, headers=alt_auth, content_type="application/json" + ) # post succeeds since value is unique to 'alt' user self.assert422(r.status_code) @@ -594,35 +620,41 @@ def test_post_resource_auth(self): self.app = Eve(settings=self.settings_file) # set auth at resource level instead. - resource_def = self.app.config['DOMAIN'][self.url] - resource_def['authentication'] = ValidBasicAuth - resource_def['auth_field'] = 'username' + resource_def = self.app.config["DOMAIN"][self.url] + resource_def["authentication"] = ValidBasicAuth + resource_def["auth_field"] = "username" # post with valid auth - must store the document with the correct # auth_field. - r = self.app.test_client().post(self.url, data=self.data, - headers=self.valid_auth, - content_type='application/json') + r = self.app.test_client().post( + self.url, + data=self.data, + headers=self.valid_auth, + content_type="application/json", + ) _, status = self.parse_response(r) # Verify that we can retrieve the same document data, status = self.parse_response( - self.app.test_client().get(self.url, headers=self.valid_auth)) + self.app.test_client().get(self.url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(len(data['_items']), 1) - self.assertEqual(data['_items'][0]['ref'], - json.loads(self.data)['ref']) + self.assertEqual(len(data["_items"]), 1) + self.assertEqual(data["_items"][0]["ref"], json.loads(self.data)["ref"]) def test_post_bandwidth_saver_off_resource_auth(self): """ Test that when BANDWIDTH_SAVER is turned off the auth_field is not exposed in the response payload """ - self.app.config['BANDWIDTH_SAVER'] = False - r = self.app.test_client().post(self.url, data=self.data, - headers=self.valid_auth, - content_type='application/json') + self.app.config["BANDWIDTH_SAVER"] = False + r = self.app.test_client().post( + self.url, + data=self.data, + headers=self.valid_auth, + content_type="application/json", + ) r, status = self.parse_response(r) - self.assertTrue('username' not in r) + self.assertTrue("username" not in r) def test_put(self): new_ref = "9999999999999999999999999" @@ -632,106 +664,129 @@ def test_put(self): data, status = self.post() # retrieve document metadata - url = '%s/%s' % (self.url, data['_id']) + url = "%s/%s" % (self.url, data["_id"]) response = self.test_client.get(url, headers=self.valid_auth) - etag = response.headers['ETag'] + etag = response.headers["ETag"] # perform put - headers = [('If-Match', etag), self.valid_auth[0]] + headers = [("If-Match", etag), self.valid_auth[0]] response, status = self.parse_response( - self.test_client.put(url, data=json.dumps(changes), - headers=headers, - content_type='application/json')) + self.test_client.put( + url, + data=json.dumps(changes), + headers=headers, + content_type="application/json", + ) + ) self.assert200(status) - etag = '"%s"' % response['_etag'] + etag = '"%s"' % response["_etag"] # document still accessible with same auth data, status = self.parse_response( - self.test_client.get(url, headers=self.valid_auth)) + self.test_client.get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['ref'], new_ref) + self.assertEqual(data["ref"], new_ref) # put on same item with different auth fails - original_auth_val = self.resource['authentication'].request_auth_value - self.resource['authentication'].request_auth_value = 'alt' - alt_auth = ('Authorization', 'Basic YWx0OnNlY3JldA==') + original_auth_val = self.resource["authentication"].request_auth_value + self.resource["authentication"].request_auth_value = "alt" + alt_auth = ("Authorization", "Basic YWx0OnNlY3JldA==") alt_changes = {"ref": "1111111111111111111111111"} - headers = [('If-Match', etag), alt_auth] + headers = [("If-Match", etag), alt_auth] response, status = self.parse_response( - self.test_client.put(url, data=json.dumps(alt_changes), - headers=headers, - content_type='application/json')) + self.test_client.put( + url, + data=json.dumps(alt_changes), + headers=headers, + content_type="application/json", + ) + ) self.assert403(status) # document still accessible with original auth - self.resource['authentication'].request_auth_value = original_auth_val + self.resource["authentication"].request_auth_value = original_auth_val data, status = self.parse_response( - self.test_client.get(url, headers=self.valid_auth)) + self.test_client.get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['ref'], new_ref) + self.assertEqual(data["ref"], new_ref) def test_put_resource_auth(self): # no global auth. self.app = Eve(settings=self.settings_file) # set auth at resource level instead. - resource_def = self.app.config['DOMAIN'][self.url] - resource_def['authentication'] = ValidBasicAuth - resource_def['auth_field'] = 'username' + resource_def = self.app.config["DOMAIN"][self.url] + resource_def["authentication"] = ValidBasicAuth + resource_def["auth_field"] = "username" # post - r = self.app.test_client().post(self.url, data=self.data, - headers=self.valid_auth, - content_type='application/json') + r = self.app.test_client().post( + self.url, + data=self.data, + headers=self.valid_auth, + content_type="application/json", + ) data, status = self.parse_response(r) # retrieve document metadata - url = '%s/%s' % (self.url, data['_id']) + url = "%s/%s" % (self.url, data["_id"]) response = self.app.test_client().get(url, headers=self.valid_auth) - etag = response.headers['ETag'] + etag = response.headers["ETag"] new_ref = "9999999999999999999999999" changes = json.dumps({"ref": new_ref}) # put - headers = [('If-Match', etag), self.valid_auth[0]] + headers = [("If-Match", etag), self.valid_auth[0]] response, status = self.parse_response( - self.app.test_client().put(url, data=json.dumps(changes), - headers=headers, - content_type='application/json')) + self.app.test_client().put( + url, + data=json.dumps(changes), + headers=headers, + content_type="application/json", + ) + ) self.assert200(status) - etag = '"%s"' % response['_etag'] + etag = '"%s"' % response["_etag"] # document still accessible with same auth data, status = self.parse_response( - self.app.test_client().get(url, headers=self.valid_auth)) + self.app.test_client().get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['ref'], new_ref) + self.assertEqual(data["ref"], new_ref) # put on same item with different auth fails - original_auth_val = resource_def['authentication'].request_auth_value - resource_def['authentication'].request_auth_value = 'alt' - alt_auth = ('Authorization', 'Basic YWx0OnNlY3JldA==') + original_auth_val = resource_def["authentication"].request_auth_value + resource_def["authentication"].request_auth_value = "alt" + alt_auth = ("Authorization", "Basic YWx0OnNlY3JldA==") alt_changes = {"ref": "1111111111111111111111111"} - headers = [('If-Match', etag), alt_auth] + headers = [("If-Match", etag), alt_auth] response, status = self.parse_response( - self.app.test_client().put(url, data=json.dumps(alt_changes), - headers=headers, - content_type='application/json')) + self.app.test_client().put( + url, + data=json.dumps(alt_changes), + headers=headers, + content_type="application/json", + ) + ) self.assert403(status) # document still accessible with original auth - resource_def['authentication'].request_auth_value = original_auth_val + resource_def["authentication"].request_auth_value = original_auth_val data, status = self.parse_response( - self.app.test_client().get(url, headers=self.valid_auth)) + self.app.test_client().get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['ref'], new_ref) + self.assertEqual(data["ref"], new_ref) def test_put_bandwidth_saver_off_resource_auth(self): """ Test that when BANDWIDTH_SAVER is turned off the auth_field is not exposed in the response payload """ - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False new_ref = "9999999999999999999999999" changes = json.dumps({"ref": new_ref}) @@ -739,34 +794,43 @@ def test_put_bandwidth_saver_off_resource_auth(self): # post document data, status = self.post() - url = '%s/%s' % (self.url, data['_id']) + url = "%s/%s" % (self.url, data["_id"]) # perform put - headers = [('If-Match', data['_etag']), self.valid_auth[0]] + headers = [("If-Match", data["_etag"]), self.valid_auth[0]] response, status = self.parse_response( - self.test_client.put(url, data=json.dumps(changes), - headers=headers, - content_type='application/json')) - self.assertTrue('username' not in response) + self.test_client.put( + url, + data=json.dumps(changes), + headers=headers, + content_type="application/json", + ) + ) + self.assertTrue("username" not in response) def test_patch(self): new_ref = "9999999999999999999999999" changes = json.dumps({"ref": new_ref}) data, status = self.post() - url = '%s/%s' % (self.url, data['_id']) + url = "%s/%s" % (self.url, data["_id"]) response = self.test_client.get(url, headers=self.valid_auth) - etag = response.headers['ETag'] - headers = [('If-Match', etag), self.valid_auth[0]] + etag = response.headers["ETag"] + headers = [("If-Match", etag), self.valid_auth[0]] response, status = self.parse_response( - self.test_client.patch(url, data=json.dumps(changes), - headers=headers, - content_type='application/json')) + self.test_client.patch( + url, + data=json.dumps(changes), + headers=headers, + content_type="application/json", + ) + ) self.assert200(status) data, status = self.parse_response( - self.test_client.get(url, headers=self.valid_auth)) + self.test_client.get(url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(data['ref'], new_ref) + self.assertEqual(data["ref"], new_ref) def test_delete(self): _db = self.connection[MONGO_DBNAME] @@ -780,22 +844,25 @@ def test_delete(self): # after the post we only get back 1 document as it's the only one we # inserted directly (others are filtered out). response, status = self.parse_response( - self.test_client.get(self.url, headers=self.valid_auth)) + self.test_client.get(self.url, headers=self.valid_auth) + ) self.assert200(status) - self.assertEqual(len(response[self.app.config['ITEMS']]), 1) + self.assertEqual(len(response[self.app.config["ITEMS"]]), 1) # delete the document we just inserted response, status = self.parse_response( - self.test_client.delete(self.url, headers=self.valid_auth)) + self.test_client.delete(self.url, headers=self.valid_auth) + ) self.assert204(status) # we now get an empty items list (other documents in collection are # filtered by auth). response, status = self.parse_response( - self.test_client.get(self.url, headers=self.valid_auth)) + self.test_client.get(self.url, headers=self.valid_auth) + ) self.assert200(status) # if it's a dict, we only got 1 item back which is expected - self.assertEqual(len(response[self.app.config['ITEMS']]), 0) + self.assertEqual(len(response[self.app.config["ITEMS"]]), 0) # make sure no other document has been deleted. cursor = _db.contacts.find() @@ -811,15 +878,15 @@ def test_delete_item(self): data, _ = self.post() # get back the document with its new etag - url = '%s/%s' % (self.url, data['_id']) + url = "%s/%s" % (self.url, data["_id"]) response = self.test_client.get(url, headers=self.valid_auth) - etag = response.headers['ETag'] - headers = [('If-Match', etag), - ('Authorization', 'Basic YWRtaW46c2VjcmV0')] + etag = response.headers["ETag"] + headers = [("If-Match", etag), ("Authorization", "Basic YWRtaW46c2VjcmV0")] # delete the document response, status = self.parse_response( - self.test_client.delete(url, headers=headers)) + self.test_client.delete(url, headers=headers) + ) self.assert204(status) # make sure no other document has been deleted. @@ -827,8 +894,10 @@ def test_delete_item(self): self.assertEqual(cursor.count(), docs_num) def post(self): - r = self.test_client.post(self.url, - data=self.data, - headers=self.valid_auth, - content_type='application/json') + r = self.test_client.post( + self.url, + data=self.data, + headers=self.valid_auth, + content_type="application/json", + ) return self.parse_response(r) diff --git a/eve/tests/config.py b/eve/tests/config.py index 181269632..d0883135c 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -13,30 +13,30 @@ class TestConfig(TestBase): def test_allow_unknown_with_soft_delete(self): my_settings = { - 'ALLOW_UNKNOWN': True, - 'SOFT_DELETE': True, - 'DOMAIN': {'contacts': {}} + "ALLOW_UNKNOWN": True, + "SOFT_DELETE": True, + "DOMAIN": {"contacts": {}}, } try: self.app = Eve(settings=my_settings) except TypeError: - self.fail("ALLOW_UNKNOWN and SOFT_DELETE enabled should not cause " - "a crash.") + self.fail( + "ALLOW_UNKNOWN and SOFT_DELETE enabled should not cause " "a crash." + ) def test_default_import_name(self): self.assertEqual(self.app.import_name, eve.__package__) def test_custom_import_name(self): - self.app = Eve('unittest', settings=self.settings_file) - self.assertEqual(self.app.import_name, 'unittest') + self.app = Eve("unittest", settings=self.settings_file) + self.assertEqual(self.app.import_name, "unittest") def test_custom_kwargs(self): - self.app = Eve('unittest', static_folder='/', - settings=self.settings_file) - self.assertEqual(self.app.static_folder, '/') + self.app = Eve("unittest", static_folder="/", settings=self.settings_file) + self.assertEqual(self.app.static_folder, "/") def test_regexconverter(self): - regex_converter = self.app.url_map.converters.get('regex') + regex_converter = self.app.url_map.converters.get("regex") self.assertEqual(regex_converter, RegexConverter) def test_default_validator(self): @@ -49,65 +49,66 @@ def test_default_settings(self): self.assertEqual(self.app.settings, self.settings_file) # TODO add tests for other global default values - self.assertEqual(self.app.config['RATE_LIMIT_GET'], None) - self.assertEqual(self.app.config['RATE_LIMIT_POST'], None) - self.assertEqual(self.app.config['RATE_LIMIT_PATCH'], None) - self.assertEqual(self.app.config['RATE_LIMIT_DELETE'], None) - - self.assertEqual(self.app.config['MONGO_HOST'], 'localhost') - self.assertEqual(self.app.config['MONGO_PORT'], 27017) - self.assertEqual(self.app.config['MONGO_QUERY_BLACKLIST'], ['$where', - '$regex']) - self.assertEqual(self.app.config['MONGO_WRITE_CONCERN'], {'w': 1}) - self.assertEqual(self.app.config['ISSUES'], '_issues') - - self.assertEqual(self.app.config['OPLOG'], False) - self.assertEqual(self.app.config['OPLOG_NAME'], 'oplog') - self.assertEqual(self.app.config['OPLOG_ENDPOINT'], None) - self.assertEqual(self.app.config['OPLOG_AUDIT'], True) - self.assertEqual(self.app.config['OPLOG_METHODS'], ['DELETE', - 'POST', - 'PATCH', - 'PUT']) - self.assertEqual(self.app.config['OPLOG_CHANGE_METHODS'], ['DELETE', - 'PATCH', - 'PUT']) - self.assertEqual(self.app.config['QUERY_WHERE'], 'where') - self.assertEqual(self.app.config['QUERY_PROJECTION'], 'projection') - self.assertEqual(self.app.config['QUERY_SORT'], 'sort') - self.assertEqual(self.app.config['QUERY_PAGE'], 'page') - self.assertEqual(self.app.config['QUERY_MAX_RESULTS'], 'max_results') - self.assertEqual(self.app.config['QUERY_EMBEDDED'], 'embedded') - self.assertEqual(self.app.config['QUERY_AGGREGATION'], 'aggregate') - - self.assertEqual(self.app.config['JSON_SORT_KEYS'], False) - self.assertEqual(self.app.config['SOFT_DELETE'], False) - self.assertEqual(self.app.config['DELETED'], '_deleted') - self.assertEqual(self.app.config['SHOW_DELETED_PARAM'], 'show_deleted') - self.assertEqual(self.app.config['STANDARD_ERRORS'], - [400, 401, 404, 405, 406, 409, 410, 412, 422, 428]) - self.assertEqual(self.app.config['UPSERT_ON_PUT'], True) - self.assertEqual(self.app.config['JSON_REQUEST_CONTENT_TYPES'], - ['application/json']) + self.assertEqual(self.app.config["RATE_LIMIT_GET"], None) + self.assertEqual(self.app.config["RATE_LIMIT_POST"], None) + self.assertEqual(self.app.config["RATE_LIMIT_PATCH"], None) + self.assertEqual(self.app.config["RATE_LIMIT_DELETE"], None) + + self.assertEqual(self.app.config["MONGO_HOST"], "localhost") + self.assertEqual(self.app.config["MONGO_PORT"], 27017) + self.assertEqual(self.app.config["MONGO_QUERY_BLACKLIST"], ["$where", "$regex"]) + self.assertEqual(self.app.config["MONGO_WRITE_CONCERN"], {"w": 1}) + self.assertEqual(self.app.config["ISSUES"], "_issues") + + self.assertEqual(self.app.config["OPLOG"], False) + self.assertEqual(self.app.config["OPLOG_NAME"], "oplog") + self.assertEqual(self.app.config["OPLOG_ENDPOINT"], None) + self.assertEqual(self.app.config["OPLOG_AUDIT"], True) + self.assertEqual( + self.app.config["OPLOG_METHODS"], ["DELETE", "POST", "PATCH", "PUT"] + ) + self.assertEqual( + self.app.config["OPLOG_CHANGE_METHODS"], ["DELETE", "PATCH", "PUT"] + ) + self.assertEqual(self.app.config["QUERY_WHERE"], "where") + self.assertEqual(self.app.config["QUERY_PROJECTION"], "projection") + self.assertEqual(self.app.config["QUERY_SORT"], "sort") + self.assertEqual(self.app.config["QUERY_PAGE"], "page") + self.assertEqual(self.app.config["QUERY_MAX_RESULTS"], "max_results") + self.assertEqual(self.app.config["QUERY_EMBEDDED"], "embedded") + self.assertEqual(self.app.config["QUERY_AGGREGATION"], "aggregate") + + self.assertEqual(self.app.config["JSON_SORT_KEYS"], False) + self.assertEqual(self.app.config["SOFT_DELETE"], False) + self.assertEqual(self.app.config["DELETED"], "_deleted") + self.assertEqual(self.app.config["SHOW_DELETED_PARAM"], "show_deleted") + self.assertEqual( + self.app.config["STANDARD_ERRORS"], + [400, 401, 404, 405, 406, 409, 410, 412, 422, 428], + ) + self.assertEqual(self.app.config["UPSERT_ON_PUT"], True) + self.assertEqual( + self.app.config["JSON_REQUEST_CONTENT_TYPES"], ["application/json"] + ) def test_settings_as_dict(self): - my_settings = {'API_VERSION': 'override!', 'DOMAIN': {'contacts': {}}} + my_settings = {"API_VERSION": "override!", "DOMAIN": {"contacts": {}}} self.app = Eve(settings=my_settings) - self.assertEqual(self.app.config['API_VERSION'], 'override!') + self.assertEqual(self.app.config["API_VERSION"], "override!") # did not reset other defaults - self.assertEqual(self.app.config['MONGO_WRITE_CONCERN'], {'w': 1}) + self.assertEqual(self.app.config["MONGO_WRITE_CONCERN"], {"w": 1}) def test_existing_env_config(self): env = os.environ - os.environ = {'EVE_SETTINGS': 'test_settings_env.py'} + os.environ = {"EVE_SETTINGS": "test_settings_env.py"} self.app = Eve() - self.assertTrue('env_domain' in self.app.config['DOMAIN']) + self.assertTrue("env_domain" in self.app.config["DOMAIN"]) os.environ = env def test_unexisting_env_config(self): env = os.environ try: - os.environ = {'EVE_SETTINGS': 'an_unexisting_pyfile.py'} + os.environ = {"EVE_SETTINGS": "an_unexisting_pyfile.py"} self.assertRaises(IOError, Eve) finally: os.environ = env @@ -115,49 +116,45 @@ def test_unexisting_env_config(self): def test_custom_validator(self): class MyTestValidator(Validator): pass - self.app = Eve(validator=MyTestValidator, - settings=self.settings_file) + + self.app = Eve(validator=MyTestValidator, settings=self.settings_file) self.assertEqual(self.app.validator, MyTestValidator) def test_custom_datalayer(self): class MyTestDataLayer(DataLayer): def init_app(self, app): pass + self.app = Eve(data=MyTestDataLayer, settings=self.settings_file) self.assertEqual(type(self.app.data), MyTestDataLayer) def test_validate_domain_struct(self): - del self.app.config['DOMAIN'] - self.assertValidateConfigFailure('missing') + del self.app.config["DOMAIN"] + self.assertValidateConfigFailure("missing") - self.app.config['DOMAIN'] = [] - self.assertValidateConfigFailure('must be a dict') + self.app.config["DOMAIN"] = [] + self.assertValidateConfigFailure("must be a dict") - self.app.config['DOMAIN'] = {} + self.app.config["DOMAIN"] = {} self.assertValidateConfigSuccess() def test_validate_resource_methods(self): - self.app.config['RESOURCE_METHODS'] = ['PUT', 'GET', 'DELETE', 'POST'] - self.assertValidateConfigFailure('PUT') + self.app.config["RESOURCE_METHODS"] = ["PUT", "GET", "DELETE", "POST"] + self.assertValidateConfigFailure("PUT") def test_validate_item_methods(self): - self.app.config['ITEM_METHODS'] = ['PUT', 'GET', 'POST', 'DELETE'] - self.assertValidateConfigFailure(['POST', 'PUT']) + self.app.config["ITEM_METHODS"] = ["PUT", "GET", "POST", "DELETE"] + self.assertValidateConfigFailure(["POST", "PUT"]) def test_validate_schema_methods(self): - test = { - 'resource_methods': ['PUT', 'GET', 'DELETE', 'POST'], - } - self.app.config['DOMAIN']['test_resource'] = test - self.assertValidateConfigFailure('PUT') + test = {"resource_methods": ["PUT", "GET", "DELETE", "POST"]} + self.app.config["DOMAIN"]["test_resource"] = test + self.assertValidateConfigFailure("PUT") def test_validate_schema_item_methods(self): - test = { - 'resource_methods': ['GET'], - 'item_methods': ['POST'], - } - self.app.config['DOMAIN']['test_resource'] = test - self.assertValidateConfigFailure('PUT') + test = {"resource_methods": ["GET"], "item_methods": ["POST"]} + self.app.config["DOMAIN"]["test_resource"] = test + self.assertValidateConfigFailure("PUT") def test_validate_datecreated_in_schema(self): self.assertUnallowedField(eve.DATE_CREATED) @@ -165,153 +162,160 @@ def test_validate_datecreated_in_schema(self): def test_validate_lastupdated_in_schema(self): self.assertUnallowedField(eve.LAST_UPDATED) - def assertUnallowedField(self, field, field_type='datetime'): + def assertUnallowedField(self, field, field_type="datetime"): self.domain.clear() - schema = {field: {'type': field_type}} - self.domain['resource'] = {'schema': schema} + schema = {field: {"type": field_type}} + self.domain["resource"] = {"schema": schema} self.app.set_defaults() - self.assertValidateSchemaFailure('resource', schema, field) + self.assertValidateSchemaFailure("resource", schema, field) def test_validate_schema(self): # lack of 'collection' key for 'data_collection' rule - schema = self.domain['invoices']['schema'] - del(schema['person']['data_relation']['resource']) - self.assertValidateSchemaFailure('invoices', schema, 'resource') + schema = self.domain["invoices"]["schema"] + del (schema["person"]["data_relation"]["resource"]) + self.assertValidateSchemaFailure("invoices", schema, "resource") def test_validate_invalid_field_names(self): - schema = self.domain['invoices']['schema'] - schema['te$t'] = {'type': 'string'} - self.assertValidateSchemaFailure('invoices', schema, 'te$t') - del(schema['te$t']) - - schema['te.t'] = {'type': 'string'} - self.assertValidateSchemaFailure('invoices', schema, 'te.t') - del(schema['te.t']) - - schema['test_a_dict_schema'] = { - 'type': 'dict', - 'schema': {'te$t': {'type': 'string'}} + schema = self.domain["invoices"]["schema"] + schema["te$t"] = {"type": "string"} + self.assertValidateSchemaFailure("invoices", schema, "te$t") + del (schema["te$t"]) + + schema["te.t"] = {"type": "string"} + self.assertValidateSchemaFailure("invoices", schema, "te.t") + del (schema["te.t"]) + + schema["test_a_dict_schema"] = { + "type": "dict", + "schema": {"te$t": {"type": "string"}}, } - self.assertValidateSchemaFailure('invoices', schema, 'te$t') + self.assertValidateSchemaFailure("invoices", schema, "te$t") - schema['test_a_dict_schema']['schema'] = {'te.t': {'type': 'string'}} - self.assertValidateSchemaFailure('invoices', schema, 'te.t') + schema["test_a_dict_schema"]["schema"] = {"te.t": {"type": "string"}} + self.assertValidateSchemaFailure("invoices", schema, "te.t") def test_set_schema_defaults(self): # default data_relation field value - schema = self.domain['invoices']['schema'] - data_relation = schema['person']['data_relation'] - self.assertTrue('field' in data_relation) - self.assertEqual(data_relation['field'], - self.domain['contacts']['id_field']) - id_field = self.domain['invoices']['id_field'] + schema = self.domain["invoices"]["schema"] + data_relation = schema["person"]["data_relation"] + self.assertTrue("field" in data_relation) + self.assertEqual(data_relation["field"], self.domain["contacts"]["id_field"]) + id_field = self.domain["invoices"]["id_field"] self.assertTrue(id_field in schema) - self.assertEqual(schema[id_field], {'type': 'objectid'}) + self.assertEqual(schema[id_field], {"type": "objectid"}) def test_set_defaults(self): self.domain.clear() - resource = 'plurals' + resource = "plurals" self.domain[resource] = {} self.app.set_defaults() self._test_defaults_for_resource(resource) settings = self.domain[resource] - self.assertEqual(len(settings['schema']), 1) + self.assertEqual(len(settings["schema"]), 1) def _test_defaults_for_resource(self, resource): settings = self.domain[resource] - self.assertEqual(settings['url'], resource) - self.assertEqual(settings['internal_resource'], - self.app.config['INTERNAL_RESOURCE']) - self.assertEqual(settings['resource_methods'], - self.app.config['RESOURCE_METHODS']) - self.assertEqual(settings['public_methods'], - self.app.config['PUBLIC_METHODS']) - self.assertEqual(settings['allowed_roles'], - self.app.config['ALLOWED_ROLES']) - self.assertEqual(settings['allowed_read_roles'], - self.app.config['ALLOWED_READ_ROLES']) - self.assertEqual(settings['allowed_write_roles'], - self.app.config['ALLOWED_WRITE_ROLES']) - self.assertEqual(settings['cache_control'], - self.app.config['CACHE_CONTROL']) - self.assertEqual(settings['cache_expires'], - self.app.config['CACHE_EXPIRES']) - self.assertEqual(settings['item_methods'], - self.app.config['ITEM_METHODS']) - self.assertEqual(settings['public_item_methods'], - self.app.config['PUBLIC_ITEM_METHODS']) - self.assertEqual(settings['allowed_item_roles'], - self.app.config['ALLOWED_ITEM_ROLES']) - self.assertEqual(settings['allowed_item_read_roles'], - self.app.config['ALLOWED_ITEM_READ_ROLES']) - self.assertEqual(settings['allowed_item_write_roles'], - self.app.config['ALLOWED_ITEM_WRITE_ROLES']) - self.assertEqual(settings['item_lookup'], - self.app.config['ITEM_LOOKUP']) - self.assertEqual(settings['item_lookup_field'], - self.app.config['ITEM_LOOKUP_FIELD']) - self.assertEqual(settings['item_url'], - self.app.config['ITEM_URL']) - self.assertEqual(settings['item_title'], - resource.rstrip('s').capitalize()) - self.assertEqual(settings['allowed_filters'], - self.app.config['ALLOWED_FILTERS']) - self.assertEqual(settings['projection'], self.app.config['PROJECTION']) - self.assertEqual(settings['versioning'], self.app.config['VERSIONING']) - self.assertEqual(settings['soft_delete'], - self.app.config['SOFT_DELETE']) - self.assertEqual(settings['sorting'], self.app.config['SORTING']) - self.assertEqual(settings['embedding'], self.app.config['EMBEDDING']) - self.assertEqual(settings['pagination'], self.app.config['PAGINATION']) - self.assertEqual(settings['auth_field'], - self.app.config['AUTH_FIELD']) - self.assertEqual(settings['allow_unknown'], - self.app.config['ALLOW_UNKNOWN']) - self.assertEqual(settings['extra_response_fields'], - self.app.config['EXTRA_RESPONSE_FIELDS']) - self.assertEqual(settings['mongo_write_concern'], - self.app.config['MONGO_WRITE_CONCERN']) - self.assertEqual(settings['resource_title'], settings['url']) - - self.assertNotEqual(settings['schema'], None) - self.assertEqual(type(settings['schema']), dict) - self.assertEqual(settings['etag_ignore_fields'], None) + self.assertEqual(settings["url"], resource) + self.assertEqual( + settings["internal_resource"], self.app.config["INTERNAL_RESOURCE"] + ) + self.assertEqual( + settings["resource_methods"], self.app.config["RESOURCE_METHODS"] + ) + self.assertEqual(settings["public_methods"], self.app.config["PUBLIC_METHODS"]) + self.assertEqual(settings["allowed_roles"], self.app.config["ALLOWED_ROLES"]) + self.assertEqual( + settings["allowed_read_roles"], self.app.config["ALLOWED_READ_ROLES"] + ) + self.assertEqual( + settings["allowed_write_roles"], self.app.config["ALLOWED_WRITE_ROLES"] + ) + self.assertEqual(settings["cache_control"], self.app.config["CACHE_CONTROL"]) + self.assertEqual(settings["cache_expires"], self.app.config["CACHE_EXPIRES"]) + self.assertEqual(settings["item_methods"], self.app.config["ITEM_METHODS"]) + self.assertEqual( + settings["public_item_methods"], self.app.config["PUBLIC_ITEM_METHODS"] + ) + self.assertEqual( + settings["allowed_item_roles"], self.app.config["ALLOWED_ITEM_ROLES"] + ) + self.assertEqual( + settings["allowed_item_read_roles"], + self.app.config["ALLOWED_ITEM_READ_ROLES"], + ) + self.assertEqual( + settings["allowed_item_write_roles"], + self.app.config["ALLOWED_ITEM_WRITE_ROLES"], + ) + self.assertEqual(settings["item_lookup"], self.app.config["ITEM_LOOKUP"]) + self.assertEqual( + settings["item_lookup_field"], self.app.config["ITEM_LOOKUP_FIELD"] + ) + self.assertEqual(settings["item_url"], self.app.config["ITEM_URL"]) + self.assertEqual(settings["item_title"], resource.rstrip("s").capitalize()) + self.assertEqual( + settings["allowed_filters"], self.app.config["ALLOWED_FILTERS"] + ) + self.assertEqual(settings["projection"], self.app.config["PROJECTION"]) + self.assertEqual(settings["versioning"], self.app.config["VERSIONING"]) + self.assertEqual(settings["soft_delete"], self.app.config["SOFT_DELETE"]) + self.assertEqual(settings["sorting"], self.app.config["SORTING"]) + self.assertEqual(settings["embedding"], self.app.config["EMBEDDING"]) + self.assertEqual(settings["pagination"], self.app.config["PAGINATION"]) + self.assertEqual(settings["auth_field"], self.app.config["AUTH_FIELD"]) + self.assertEqual(settings["allow_unknown"], self.app.config["ALLOW_UNKNOWN"]) + self.assertEqual( + settings["extra_response_fields"], self.app.config["EXTRA_RESPONSE_FIELDS"] + ) + self.assertEqual( + settings["mongo_write_concern"], self.app.config["MONGO_WRITE_CONCERN"] + ) + self.assertEqual(settings["resource_title"], settings["url"]) + + self.assertNotEqual(settings["schema"], None) + self.assertEqual(type(settings["schema"]), dict) + self.assertEqual(settings["etag_ignore_fields"], None) def test_datasource(self): - self._test_datasource_for_resource('invoices') + self._test_datasource_for_resource("invoices") def _test_datasource_for_resource(self, resource): - datasource = self.domain[resource]['datasource'] - schema = self.domain[resource]['schema'] - compare = [key for key in datasource['projection'] if key in schema] - compare.extend([self.domain[resource]['id_field'], - self.app.config['LAST_UPDATED'], - self.app.config['DATE_CREATED'], - self.app.config['ETAG']]) + datasource = self.domain[resource]["datasource"] + schema = self.domain[resource]["schema"] + compare = [key for key in datasource["projection"] if key in schema] + compare.extend( + [ + self.domain[resource]["id_field"], + self.app.config["LAST_UPDATED"], + self.app.config["DATE_CREATED"], + self.app.config["ETAG"], + ] + ) - self.assertEqual(datasource['projection'], - dict((field, 1) for (field) in compare)) - self.assertEqual(datasource['source'], resource) - self.assertEqual(datasource['filter'], None) + self.assertEqual( + datasource["projection"], dict((field, 1) for (field) in compare) + ) + self.assertEqual(datasource["source"], resource) + self.assertEqual(datasource["filter"], None) - self.assertEqual(datasource['aggregation'], None) + self.assertEqual(datasource["aggregation"], None) def test_validate_roles(self): for resource in self.domain: - self.assertValidateRoles(resource, 'allowed_roles') - self.assertValidateRoles(resource, 'allowed_read_roles') - self.assertValidateRoles(resource, 'allowed_write_roles') - self.assertValidateRoles(resource, 'allowed_item_roles') - self.assertValidateRoles(resource, 'allowed_item_read_roles') - self.assertValidateRoles(resource, 'allowed_item_write_roles') + self.assertValidateRoles(resource, "allowed_roles") + self.assertValidateRoles(resource, "allowed_read_roles") + self.assertValidateRoles(resource, "allowed_write_roles") + self.assertValidateRoles(resource, "allowed_item_roles") + self.assertValidateRoles(resource, "allowed_item_read_roles") + self.assertValidateRoles(resource, "allowed_item_write_roles") def assertValidateRoles(self, resource, directive): prev = self.domain[resource][directive] - self.domain[resource][directive] = 'admin' + self.domain[resource][directive] = "admin" self.assertValidateConfigFailure(directive) self.domain[resource][directive] = [] self.assertValidateConfigSuccess() - self.domain[resource][directive] = ['admin', 'dev'] + self.domain[resource][directive] = ["admin", "dev"] self.assertValidateConfigSuccess() self.domain[resource][directive] = None self.assertValidateConfigFailure(directive) @@ -322,7 +326,7 @@ def assertValidateConfigSuccess(self): self.app.validate_domain_struct() self.app.validate_config() except ConfigException as e: - self.fail('ConfigException not expected: %s' % e) + self.fail("ConfigException not expected: %s" % e) def assertValidateConfigFailure(self, expected): try: @@ -345,54 +349,48 @@ def assertValidateSchemaFailure(self, resource, schema, expected): self.fail("SchemaException expected but not raised.") def test_url_helpers(self): - self.assertNotEqual(self.app.config.get('URLS'), None) - self.assertEqual(type(self.app.config['URLS']), dict) + self.assertNotEqual(self.app.config.get("URLS"), None) + self.assertEqual(type(self.app.config["URLS"]), dict) - self.assertNotEqual(self.app.config.get('SOURCES'), None) - self.assertEqual(type(self.app.config['SOURCES']), dict) + self.assertNotEqual(self.app.config.get("SOURCES"), None) + self.assertEqual(type(self.app.config["SOURCES"]), dict) - del(self.domain['internal_transactions']) + del (self.domain["internal_transactions"]) for resource, settings in self.domain.items(): - self.assertEqual(settings['datasource'], - self.app.config['SOURCES'][resource]) + self.assertEqual( + settings["datasource"], self.app.config["SOURCES"][resource] + ) def test_pretty_resource_urls(self): """ test that regexes are stripped out of urls and #466 is fixed. """ - resource_url = self.app.config['URLS']['peopleinvoices'] - pretty_url = 'users//invoices' + resource_url = self.app.config["URLS"]["peopleinvoices"] + pretty_url = "users//invoices" self.assertEqual(resource_url, pretty_url) - resource_url = self.app.config['URLS']['peoplesearches'] - pretty_url = 'users//saved_searches' + resource_url = self.app.config["URLS"]["peoplesearches"] + pretty_url = "users//saved_searches" self.assertEqual(resource_url, pretty_url) def test_url_rules(self): - map_adapter = self.app.url_map.bind('') + map_adapter = self.app.url_map.bind("") - del(self.domain['peopleinvoices']) - del(self.domain['peoplerequiredinvoices']) - del(self.domain['peoplesearches']) - del(self.domain['internal_transactions']) - del(self.domain['child_products']) + del (self.domain["peopleinvoices"]) + del (self.domain["peoplerequiredinvoices"]) + del (self.domain["peoplesearches"]) + del (self.domain["internal_transactions"]) + del (self.domain["child_products"]) for _, settings in self.domain.items(): - for method in settings['resource_methods']: - self.assertTrue(map_adapter.test('/%s/' % settings['url'], - method)) + for method in settings["resource_methods"]: + self.assertTrue(map_adapter.test("/%s/" % settings["url"], method)) # TODO test item endpoints as well. gonna be tricky since # we have to reverse regexes here. will be fun. def test_register_resource(self): - resource = 'resource' + resource = "resource" settings = { - 'schema': { - 'title': { - 'type': 'string', - 'default': 'Mr.', - }, - 'price': { - 'type': 'integer', - 'default': 100 - }, + "schema": { + "title": {"type": "string", "default": "Mr."}, + "price": {"type": "integer", "default": 100}, } } self.app.register_resource(resource, settings) @@ -401,107 +399,106 @@ def test_register_resource(self): self.test_validate_roles() def test_auth_field_as_idfield(self): - resource = 'resource' - settings = { - 'auth_field': self.app.config['ID_FIELD'], - } - self.assertRaises(ConfigException, self.app.register_resource, - resource, settings) + resource = "resource" + settings = {"auth_field": self.app.config["ID_FIELD"]} + self.assertRaises( + ConfigException, self.app.register_resource, resource, settings + ) def test_auth_field_as_custom_idfield(self): - resource = 'resource' + resource = "resource" settings = { - 'schema': { - 'id': {'type': 'string'} - }, - 'id_field': 'id', - 'auth_field': 'id' + "schema": {"id": {"type": "string"}}, + "id_field": "id", + "auth_field": "id", } - self.assertRaises(ConfigException, self.app.register_resource, - resource, settings) + self.assertRaises( + ConfigException, self.app.register_resource, resource, settings + ) def test_oplog_config(self): # if OPLOG_ENDPOINT is eanbled the endoint is included with the domain - self.app.config['OPLOG_ENDPOINT'] = 'oplog' + self.app.config["OPLOG_ENDPOINT"] = "oplog" self.app._init_oplog() - self.assertOplog('oplog', 'oplog') - del(self.domain['oplog']) + self.assertOplog("oplog", "oplog") + del (self.domain["oplog"]) # OPLOG can be also with a custom name (which will be used # as the collection/table name on the db) - oplog = 'custom' - self.app.config['OPLOG_NAME'] = oplog + oplog = "custom" + self.app.config["OPLOG_NAME"] = oplog self.app._init_oplog() - self.assertOplog(oplog, 'oplog') - del(self.domain[oplog]) + self.assertOplog(oplog, "oplog") + del (self.domain[oplog]) # oplog can be defined as a regular API endpoint, with a couple caveats - self.domain['oplog'] = { - 'resource_methods': ['POST', 'DELETE'], # not allowed - 'resource_items': ['PATCH', 'PUT'], # not allowed - 'url': 'custom_url', - 'datasource': {'source': 'customsource'} + self.domain["oplog"] = { + "resource_methods": ["POST", "DELETE"], # not allowed + "resource_items": ["PATCH", "PUT"], # not allowed + "url": "custom_url", + "datasource": {"source": "customsource"}, } - self.app.config['OPLOG_NAME'] = 'oplog' - settings = self.domain['oplog'] + self.app.config["OPLOG_NAME"] = "oplog" + settings = self.domain["oplog"] self.app._init_oplog() # endpoint is always read-only - self.assertEqual(settings['resource_methods'], ['GET']) - self.assertEqual(settings['item_methods'], ['GET']) + self.assertEqual(settings["resource_methods"], ["GET"]) + self.assertEqual(settings["item_methods"], ["GET"]) # other settings are customizable - self.assertEqual(settings['url'], 'custom_url') - self.assertEqual(settings['datasource']['source'], 'customsource') + self.assertEqual(settings["url"], "custom_url") + self.assertEqual(settings["datasource"]["source"], "customsource") def assertOplog(self, key, endpoint): self.assertTrue(key in self.domain) settings = self.domain[key] - self.assertEqual(settings['resource_methods'], ['GET']) - self.assertEqual(settings['item_methods'], ['GET']) - self.assertEqual(settings['url'], endpoint) - self.assertEqual(settings['datasource']['source'], key) + self.assertEqual(settings["resource_methods"], ["GET"]) + self.assertEqual(settings["item_methods"], ["GET"]) + self.assertEqual(settings["url"], endpoint) + self.assertEqual(settings["datasource"]["source"], key) def test_create_indexes(self): # prepare a specific schema with mongo indexes declared # along with the schema. settings = { - 'schema': { - 'name': {'type': 'string'}, - 'other_field': {'type': 'string'}, - 'lat_long': {'type': 'list'} + "schema": { + "name": {"type": "string"}, + "other_field": {"type": "string"}, + "lat_long": {"type": "list"}, + }, + "versioning": True, + "mongo_indexes": { + "name": [("name", 1)], + "composed": [("name", 1), ("other_field", 1)], + "arguments": ([("lat_long", "2d")], {"sparse": True}), }, - 'versioning': True, - 'mongo_indexes': { - 'name': [('name', 1)], - 'composed': [('name', 1), ('other_field', 1)], - 'arguments': ([('lat_long', "2d")], {"sparse": True}) - } } - self.app.register_resource('mongodb_features', settings) + self.app.register_resource("mongodb_features", settings) # check that the indexes are there as a part of the resource # settings self.assertEqual( - self.app.config['DOMAIN']['mongodb_features']['mongo_indexes'], - settings['mongo_indexes'] + self.app.config["DOMAIN"]["mongodb_features"]["mongo_indexes"], + settings["mongo_indexes"], ) # check that the indexes were created from pymongo import MongoClient - db_name = self.app.config['MONGO_DBNAME'] + + db_name = self.app.config["MONGO_DBNAME"] db = MongoClient()[db_name] - for coll in [db['mongodb_features'], db['mongodb_features_versions']]: + for coll in [db["mongodb_features"], db["mongodb_features_versions"]]: indexes = coll.index_information() # at least there is an index for the _id field plus the indexes # created by the resource of this test - self.assertTrue(len(indexes) > len(settings['mongo_indexes'])) + self.assertTrue(len(indexes) > len(settings["mongo_indexes"])) # check each one, fields involved and arguments given - for key, value in settings['mongo_indexes'].items(): + for key, value in settings["mongo_indexes"].items(): if isinstance(value, tuple): fields, args = value else: @@ -509,7 +506,7 @@ def test_create_indexes(self): args = None self.assertTrue(key in indexes) - self.assertEqual(indexes[key]['key'], fields) + self.assertEqual(indexes[key]["key"], fields) for arg in args or (): self.assertTrue(arg in indexes[key]) @@ -519,7 +516,7 @@ def test_custom_error_handlers(self): """ Test that the standard, custom error handler is registered for supported error codes. """ - codes = self.app.config['STANDARD_ERRORS'] + codes = self.app.config["STANDARD_ERRORS"] # http://flask.pocoo.org/docs/0.10/api/#flask.Flask.error_handler_spec handlers = self.app.error_handler_spec[None] @@ -529,36 +526,23 @@ def test_custom_error_handlers(self): def test_mongodb_settings(self): # Create custom app with mongodb settings. - settings = { - 'DOMAIN': {'contacts': {}}, - 'MONGO_OPTIONS': { - 'connect': False - } - } + settings = {"DOMAIN": {"contacts": {}}, "MONGO_OPTIONS": {"connect": False}} app = Eve(settings=settings) # Check if settings are set. self.assertEqual( - app.config['MONGO_OPTIONS']['connect'], - app.config['MONGO_CONNECT'] + app.config["MONGO_OPTIONS"]["connect"], app.config["MONGO_CONNECT"] ) # Prepare a specific schema with mongo specific settings. settings = { - 'schema': { - 'name': {'type': 'string'}, - }, - 'MONGO_OPTIONS': { - 'connect': False - } + "schema": {"name": {"type": "string"}}, + "MONGO_OPTIONS": {"connect": False}, } - self.app.register_resource('mongodb_settings', settings) + self.app.register_resource("mongodb_settings", settings) # check that settings are set. - resource_settings = self.app.config['DOMAIN']['mongodb_settings'] - self.assertEqual( - resource_settings['MONGO_OPTIONS'], - settings['MONGO_OPTIONS'] - ) + resource_settings = self.app.config["DOMAIN"]["mongodb_settings"] + self.assertEqual(resource_settings["MONGO_OPTIONS"], settings["MONGO_OPTIONS"]) # check that settings are set. self.assertEqual( - resource_settings['MONGO_OPTIONS']['connect'], - settings['MONGO_OPTIONS']['connect'] + resource_settings["MONGO_OPTIONS"]["connect"], + settings["MONGO_OPTIONS"]["connect"], ) diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 67751a357..9fe22fc32 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -18,6 +18,7 @@ class UUIDEncoder(BaseJSONEncoder): This is different from BaseJSONEoncoder since it also addresses encoding of UUID """ + def default(self, obj): if isinstance(obj, UUID): return str(obj) @@ -45,6 +46,7 @@ class UUIDValidator(Validator): """ Extends the base mongo validator adding support for the uuid data-type """ + def _validate_type_uuid(self, value): try: UUID(value) @@ -60,29 +62,25 @@ class TestCustomConverters(TestMinimal): def setUp(self): uuids = { - 'resource_methods': ['GET', 'POST'], - 'item_methods': ['GET', 'PATCH', 'PUT', 'DELETE'], - 'item_url': 'uuid', - 'schema': { - '_id': {'type': 'uuid'}, - 'name': {'type': 'string'} - } + "resource_methods": ["GET", "POST"], + "item_methods": ["GET", "PATCH", "PUT", "DELETE"], + "item_url": "uuid", + "schema": {"_id": {"type": "uuid"}, "name": {"type": "string"}}, } settings = { - 'MONGO_USERNAME': 'test_user', - 'MONGO_PASSWORD': 'test_pw', - 'MONGO_DBNAME': 'eve_test', - 'DOMAIN': { - 'uuids': uuids - } + "MONGO_USERNAME": "test_user", + "MONGO_PASSWORD": "test_pw", + "MONGO_DBNAME": "eve_test", + "DOMAIN": {"uuids": uuids}, } - url_converters = {'uuid': UUIDConverter} - self.uuid_valid = '48c00ee9-4dbe-413f-9fc3-d5f12a91de1c' - self.url = '/uuids/%s' % self.uuid_valid - self.headers = [('Content-Type', 'application/json')] + url_converters = {"uuid": UUIDConverter} + self.uuid_valid = "48c00ee9-4dbe-413f-9fc3-d5f12a91de1c" + self.url = "/uuids/%s" % self.uuid_valid + self.headers = [("Content-Type", "application/json")] - super(TestCustomConverters, self).setUp(settings_file=settings, - url_converters=url_converters) + super(TestCustomConverters, self).setUp( + settings_file=settings, url_converters=url_converters + ) self.app.validator = UUIDValidator self.app.data.json_encoder_class = UUIDEncoder @@ -91,7 +89,7 @@ def bulk_insert(self): # create a document which has a id field of UUID type and store it # into the database _db = self.connection[MONGO_DBNAME] - _db.uuids.insert_one({'_id': UUID(self.uuid_valid)}) + _db.uuids.insert_one({"_id": UUID(self.uuid_valid)}) def _get_etag(self): r = self.test_client.get(self.url) @@ -104,61 +102,60 @@ def test_get_uuid(self): def test_patch_uuid(self): etag = self._get_etag() - self.headers.append(('If-Match', etag)) - r = self.test_client.patch(self.url, - data=json.dumps({"name": " a_name"}), - headers=self.headers) + self.headers.append(("If-Match", etag)) + r = self.test_client.patch( + self.url, data=json.dumps({"name": " a_name"}), headers=self.headers + ) self.assert200(r.status_code) def test_put_uuid(self): etag = self._get_etag() - self.headers.append(('If-Match', etag)) - r = self.test_client.put(self.url, - data=json.dumps({"name": " a_name"}), - headers=self.headers) + self.headers.append(("If-Match", etag)) + r = self.test_client.put( + self.url, data=json.dumps({"name": " a_name"}), headers=self.headers + ) self.assert200(r.status_code) def test_delete_uuid(self): etag = self._get_etag() - self.headers.append(('If-Match', etag)) + self.headers.append(("If-Match", etag)) r = self.test_client.delete(self.url, headers=self.headers) self.assert204(r.status_code) def test_post_uuid(self): - new_id = '48c00ee9-4dbe-413f-9fc3-d5f12a91de13' - data = json.dumps({'_id': new_id}) - r = self.test_client.post('uuids', data=data, headers=self.headers) + new_id = "48c00ee9-4dbe-413f-9fc3-d5f12a91de13" + data = json.dumps({"_id": new_id}) + r = self.test_client.post("uuids", data=data, headers=self.headers) self.assert201(r.status_code) - match_id = json.loads(r.get_data())['_id'] + match_id = json.loads(r.get_data())["_id"] self.assertEqual(new_id, match_id) class TestEndPoints(TestBase): - def test_homepage(self): - r = self.test_client.get('/') + r = self.test_client.get("/") self.assertEqual(r.status_code, 200) def test_resource_endpoint(self): - del(self.domain['peopleinvoices']) - del(self.domain['peoplerequiredinvoices']) - del(self.domain['peoplesearches']) - del(self.domain['internal_transactions']) - del(self.domain['child_products']) + del (self.domain["peopleinvoices"]) + del (self.domain["peoplerequiredinvoices"]) + del (self.domain["peoplesearches"]) + del (self.domain["internal_transactions"]) + del (self.domain["child_products"]) for settings in self.domain.values(): - r = self.test_client.get('/%s/' % settings['url']) + r = self.test_client.get("/%s/" % settings["url"]) self.assert200(r.status_code) - r = self.test_client.get('/%s' % settings['url']) + r = self.test_client.get("/%s" % settings["url"]) self.assert200(r.status_code) def assert_item_fields(self, data, resource=None): - id_field = self.domain[resource or self.known_resource]['id_field'] + id_field = self.domain[resource or self.known_resource]["id_field"] self.assertTrue(id_field in list(data)) - self.assertTrue('_created' in list(data)) - self.assertTrue('_updated' in list(data)) - self.assertTrue('_etag' in list(data)) - self.assertTrue('_links' in list(data)) + self.assertTrue("_created" in list(data)) + self.assertTrue("_updated" in list(data)) + self.assertTrue("_etag" in list(data)) + self.assertTrue("_links" in list(data)) def test_item_endpoint_id(self): data, status_code = self.get(self.known_resource, item=self.item_id) @@ -172,13 +169,12 @@ def test_item_endpoint_additional_lookup(self): def test_item_self_link(self): data, status_code = self.get(self.known_resource, item=self.item_id) - lookup_field = self.domain[self.known_resource]['item_lookup_field'] - link = '%s/%s' % (self.known_resource_url.lstrip('/'), - self.item[lookup_field]) - self.assertEqual(data.get('_links').get('self').get('href'), link) + lookup_field = self.domain[self.known_resource]["item_lookup_field"] + link = "%s/%s" % (self.known_resource_url.lstrip("/"), self.item[lookup_field]) + self.assertEqual(data.get("_links").get("self").get("href"), link) def test_unknown_endpoints(self): - r = self.test_client.get('/%s/' % self.unknown_resource) + r = self.test_client.get("/%s/" % self.unknown_resource) self.assert404(r.status_code) r = self.test_client.get(self.unknown_item_id_url) @@ -188,70 +184,69 @@ def test_unknown_endpoints(self): self.assert404(r.status_code) def test_api_version(self): - settings_file = os.path.join(self.this_directory, 'test_version.py') + settings_file = os.path.join(self.this_directory, "test_version.py") self.app = Eve(settings=settings_file) self.test_prefix = self.app.test_client() - r = self.test_prefix.get('/') + r = self.test_prefix.get("/") self.assert404(r.status_code) - r = self.test_prefix.get('/v1/') + r = self.test_prefix.get("/v1/") self.assert200(r.status_code) - r = self.test_prefix.get('/contacts/') + r = self.test_prefix.get("/contacts/") self.assert404(r.status_code) - r = self.test_prefix.get('/v1/contacts') + r = self.test_prefix.get("/v1/contacts") self.assert200(r.status_code) - r = self.test_prefix.get('/v1/contacts/') + r = self.test_prefix.get("/v1/contacts/") self.assert200(r.status_code) def test_api_prefix(self): - settings_file = os.path.join(self.this_directory, 'test_prefix.py') + settings_file = os.path.join(self.this_directory, "test_prefix.py") self.app = Eve(settings=settings_file) self.test_prefix = self.app.test_client() - r = self.test_prefix.get('/') + r = self.test_prefix.get("/") self.assert404(r.status_code) - r = self.test_prefix.get('/prefix/') + r = self.test_prefix.get("/prefix/") self.assert200(r.status_code) - r = self.test_prefix.get('/prefix/contacts') + r = self.test_prefix.get("/prefix/contacts") self.assert200(r.status_code) - r = self.test_prefix.get('/prefix/contacts/') + r = self.test_prefix.get("/prefix/contacts/") self.assert200(r.status_code) - r = self.test_prefix.post('/prefix/contacts/', data='{}', - content_type='application/json') + r = self.test_prefix.post( + "/prefix/contacts/", data="{}", content_type="application/json" + ) self.assert201(r.status_code) def test_api_prefix_post_internal(self): # https://github.com/pyeve/eve/issues/810 from eve.methods.post import post_internal - settings_file = os.path.join(self.this_directory, 'test_prefix.py') + settings_file = os.path.join(self.this_directory, "test_prefix.py") self.app = Eve(settings=settings_file) self.test_prefix = self.app.test_client() # This works fine - with self.app.test_request_context( - method='POST', path='/prefix/contacts'): - _, _, _, status_code, _ = post_internal('contacts', {}) + with self.app.test_request_context(method="POST", path="/prefix/contacts"): + _, _, _, status_code, _ = post_internal("contacts", {}) self.assert201(status_code) # This fails unless #810 is fixed with self.app.test_request_context(): - _, _, _, status_code, _ = post_internal('contacts', {}) + _, _, _, status_code, _ = post_internal("contacts", {}) self.assert201(status_code) def test_api_prefix_version(self): - settings_file = os.path.join(self.this_directory, - 'test_prefix_version.py') + settings_file = os.path.join(self.this_directory, "test_prefix_version.py") self.app = Eve(settings=settings_file) self.test_prefix = self.app.test_client() - r = self.test_prefix.get('/') + r = self.test_prefix.get("/") self.assert404(r.status_code) - r = self.test_prefix.get('/prefix/v1/') + r = self.test_prefix.get("/prefix/v1/") self.assert200(r.status_code) - r = self.test_prefix.get('/prefix/v1/contacts') + r = self.test_prefix.get("/prefix/v1/contacts") self.assert200(r.status_code) - r = self.test_prefix.get('/prefix/v1/contacts/') + r = self.test_prefix.get("/prefix/v1/contacts/") self.assert200(r.status_code) def test_api_prefix_version_hateoas_links(self): @@ -259,97 +254,93 @@ def test_api_prefix_version_hateoas_links(self): out of hateoas links since they are now relative to the API entry point (root). """ - settings_file = os.path.join(self.this_directory, - 'test_prefix_version.py') + settings_file = os.path.join(self.this_directory, "test_prefix_version.py") self.app = Eve(settings=settings_file) self.test_prefix = self.app.test_client() - r = self.test_prefix.get('/prefix/v1/') - href = json.loads(r.get_data())['_links']['child'][0]['href'] - self.assertEqual(href, 'contacts') + r = self.test_prefix.get("/prefix/v1/") + href = json.loads(r.get_data())["_links"]["child"][0]["href"] + self.assertEqual(href, "contacts") - r = self.test_prefix.get('/prefix/v1/contacts') - href = json.loads(r.get_data())['_links']['self']['href'] - self.assertEqual(href, 'contacts') + r = self.test_prefix.get("/prefix/v1/contacts") + href = json.loads(r.get_data())["_links"]["self"]["href"] + self.assertEqual(href, "contacts") def test_nested_endpoint(self): - r = self.test_client.get('/users/overseas') + r = self.test_client.get("/users/overseas") self.assert200(r.status_code) def test_homepage_does_not_have_internal_resources(self): - r = self.test_client.get('/') + r = self.test_client.get("/") links = json.loads(r.get_data()) for resource in self.domain.keys(): - internal = self.domain[resource].get('internal_resource', False) + internal = self.domain[resource].get("internal_resource", False) if internal: self.assertFalse(internal in links.keys()) def on_generic_inserted(self, resource, docs): - if resource != 'internal_transactions': + if resource != "internal_transactions": dt = datetime.now() transaction = { - 'entities': [doc['_id'] for doc in docs], - 'original_resource': resource, + "entities": [doc["_id"] for doc in docs], + "original_resource": resource, config.LAST_UPDATED: dt, config.DATE_CREATED: dt, } - self.app.data.insert('internal_transactions', transaction) + self.app.data.insert("internal_transactions", transaction) def test_internal_endpoint(self): self.app.on_inserted -= self.on_generic_inserted self.app.on_inserted += self.on_generic_inserted - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "rows" - test_value = [ - {'sku': 'AT1234', 'price': 99}, - {'sku': 'XF9876', 'price': 9999} - ] + test_value = [{"sku": "AT1234", "price": 99}, {"sku": "XF9876", "price": 9999}] data = {test_field: test_value} resp_data, code = self.post(self.known_resource_url, data) self.assert201(code) def test_oplog_endpoint(self): - r = self.test_client.get('/oplog') + r = self.test_client.get("/oplog") self.assert404(r.status_code) - self.app.config['OPLOG_ENDPOINT'] = 'oplog' + self.app.config["OPLOG_ENDPOINT"] = "oplog" self.app._init_oplog() - settings = self.app.config['DOMAIN']['oplog'] - self.app.register_resource('oplog', settings) - r = self.test_client.get('/oplog') + settings = self.app.config["DOMAIN"]["oplog"] + self.app.register_resource("oplog", settings) + r = self.test_client.get("/oplog") self.assert200(r.status_code) # OPLOG endpoint is read-only - data = {'field': 'value'} - _, status_code = self.post('/oplog', data) + data = {"field": "value"} + _, status_code = self.post("/oplog", data) self.assert405(status_code) - _, status_code = self.delete('/oplog') + _, status_code = self.delete("/oplog") self.assert405(status_code) def test_schema_endpoint(self): - known_schema_path = '/schema/%s' % self.known_resource + known_schema_path = "/schema/%s" % self.known_resource r = self.test_client.get(known_schema_path) self.assert404(r.status_code) - self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() r = self.test_client.get(known_schema_path) self.assert200(r.status_code) - self.assertEqual(r.mimetype, 'application/json') + self.assertEqual(r.mimetype, "application/json") self.assertEqual( - json.loads(r.data), - self.app.config['DOMAIN'][self.known_resource]['schema']) + json.loads(r.data), self.app.config["DOMAIN"][self.known_resource]["schema"] + ) - r = self.test_client.get('/schema/%s' % self.unknown_resource) + r = self.test_client.get("/schema/%s" % self.unknown_resource) self.assert404(r.status_code) # schema endpoint doesn't reveal internal resources - r = self.test_client.get('/schema/internal_transactions') + r = self.test_client.get("/schema/internal_transactions") self.assert404(r.status_code) # schema endpoint is read-only - data = {'field': 'value'} + data = {"field": "value"} _, status_code = self.patch(known_schema_path, data) self.assert405(status_code) _, status_code = self.put(known_schema_path, data) @@ -360,15 +351,14 @@ def test_schema_endpoint(self): self.assert405(status_code) def test_schema_endpoint_does_not_attempt_callable_serialization(self): - self.domain[self.known_resource]['schema']['lambda'] = { - 'type': 'boolean', - 'coerce': lambda v: v if type(v) is bool else v.lower() in ['true', - '1'] + self.domain[self.known_resource]["schema"]["lambda"] = { + "type": "boolean", + "coerce": lambda v: v if type(v) is bool else v.lower() in ["true", "1"], } - known_schema_path = '/schema/%s' % self.known_resource - self.app.config['SCHEMA_ENDPOINT'] = 'schema' + known_schema_path = "/schema/%s" % self.known_resource + self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() r = self.test_client.get(known_schema_path) self.assert200(r.status_code) - self.assertEqual(json.loads(r.data)['lambda']['coerce'], '') + self.assertEqual(json.loads(r.data)["lambda"]["coerce"], "") diff --git a/eve/tests/io/flask_pymongo.py b/eve/tests/io/flask_pymongo.py index 93ef39182..b64ca5adc 100644 --- a/eve/tests/io/flask_pymongo.py +++ b/eve/tests/io/flask_pymongo.py @@ -1,8 +1,13 @@ from eve.tests import TestBase from pymongo import MongoClient from pymongo.errors import OperationFailure -from eve.tests.test_settings import MONGO1_DBNAME, MONGO1_USERNAME, \ - MONGO1_PASSWORD, MONGO_HOST, MONGO_PORT +from eve.tests.test_settings import ( + MONGO1_DBNAME, + MONGO1_USERNAME, + MONGO1_PASSWORD, + MONGO_HOST, + MONGO_PORT, +) from eve.io.mongo.flask_pymongo import PyMongo @@ -10,52 +15,48 @@ class TestPyMongo(TestBase): def setUp(self, url_converters=None): super(TestPyMongo, self).setUp(url_converters) self._setupdb() - schema = { - 'title': {'type': 'string'}, - } - settings = { - 'schema': schema, - 'mongo_prefix': 'MONGO1', - } + schema = {"title": {"type": "string"}} + settings = {"schema": schema, "mongo_prefix": "MONGO1"} - self.app.register_resource('works', settings) + self.app.register_resource("works", settings) def test_auth_params_provided_in_mongo_url(self): - self.app.config['MONGO1_URL'] = \ - 'mongodb://%s:%s@%s:%s' % (MONGO1_USERNAME, MONGO1_PASSWORD, - MONGO_HOST, MONGO_PORT) + self.app.config["MONGO1_URL"] = "mongodb://%s:%s@%s:%s" % ( + MONGO1_USERNAME, + MONGO1_PASSWORD, + MONGO_HOST, + MONGO_PORT, + ) with self.app.app_context(): - db = PyMongo(self.app, 'MONGO1').db + db = PyMongo(self.app, "MONGO1").db self.assertEqual(0, db.works.count()) def test_auth_params_provided_in_config(self): - self.app.config['MONGO1_USERNAME'] = MONGO1_USERNAME - self.app.config['MONGO1_PASSWORD'] = MONGO1_PASSWORD + self.app.config["MONGO1_USERNAME"] = MONGO1_USERNAME + self.app.config["MONGO1_PASSWORD"] = MONGO1_PASSWORD with self.app.app_context(): - db = PyMongo(self.app, 'MONGO1').db + db = PyMongo(self.app, "MONGO1").db self.assertEqual(0, db.works.count()) def test_invalid_auth_params_provided(self): # if bad username and/or password is provided in MONGO_URL and mongo # run w\o --auth pymongo won't raise exception - self.app.config['MONGO1_USERNAME'] = 'bad_username' - self.app.config['MONGO1_PASSWORD'] = 'bad_password' + self.app.config["MONGO1_USERNAME"] = "bad_username" + self.app.config["MONGO1_PASSWORD"] = "bad_password" self.assertRaises(OperationFailure, self._pymongo_instance) def test_invalid_port(self): - self.app.config['MONGO1_PORT'] = 'bad_value' + self.app.config["MONGO1_PORT"] = "bad_value" self.assertRaises(TypeError, self._pymongo_instance) def test_invalid_options(self): - self.app.config['MONGO1_OPTIONS'] = { - 'connectTimeoutMS': 'bad_value' - } + self.app.config["MONGO1_OPTIONS"] = {"connectTimeoutMS": "bad_value"} self.assertRaises(ValueError, self._pymongo_instance) def test_valid_port(self): - self.app.config['MONGO1_PORT'] = 27017 + self.app.config["MONGO1_PORT"] = 27017 with self.app.app_context(): - db = PyMongo(self.app, 'MONGO1').db + db = PyMongo(self.app, "MONGO1").db self.assertEqual(0, db.works.count()) def _setupdb(self): @@ -63,12 +64,13 @@ def _setupdb(self): self.connection.drop_database(MONGO1_DBNAME) db = self.connection[MONGO1_DBNAME] try: - db.command('dropUser', MONGO1_USERNAME) + db.command("dropUser", MONGO1_USERNAME) except OperationFailure: pass - db.command('createUser', MONGO1_USERNAME, pwd=MONGO1_PASSWORD, - roles=['dbAdmin']) + db.command( + "createUser", MONGO1_USERNAME, pwd=MONGO1_PASSWORD, roles=["dbAdmin"] + ) def _pymongo_instance(self): with self.app.app_context(): - PyMongo(self.app, 'MONGO1') + PyMongo(self.app, "MONGO1") diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 7c21736ce..2f2837405 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -27,14 +27,14 @@ def setUp(self): super(TestGridFSMediaStorage, self).setUp() self.url = self.known_resource_url self.resource = self.known_resource - self.headers = [('Content-Type', 'multipart/form-data')] - self.id_field = self.domain[self.resource]['id_field'] - self.test_field, self.test_value = 'ref', "1234567890123456789054321" + self.headers = [("Content-Type", "multipart/form-data")] + self.id_field = self.domain[self.resource]["id_field"] + self.test_field, self.test_value = "ref", "1234567890123456789054321" # we want an explicit binary as Py3 encodestring() expects binaries. - self.clean = b'my file contents' + self.clean = b"my file contents" # encodedstring will raise a DeprecationWarning under Python3.3, but # the alternative encodebytes is not available in Python 2. - self.encoded = base64.encodestring(self.clean).decode('utf-8') + self.encoded = base64.encodestring(self.clean).decode("utf-8") def test_gridfs_media_storage_errors(self): self.assertRaises(TypeError, GridFSMediaStorage) @@ -42,15 +42,16 @@ def test_gridfs_media_storage_errors(self): def test_gridfs_media_storage_post(self): # send something different than a file and get an error back - data = {'media': 'not a file'} + data = {"media": "not a file"} r, s = self.parse_response( - self.test_client.post(self.url, data=data, headers=self.headers)) + self.test_client.post(self.url, data=data, headers=self.headers) + ) self.assertEqual(STATUS_ERR, r[STATUS]) # validates media fields - self.assertTrue('must be of media type' in r[ISSUES]['media']) + self.assertTrue("must be of media type" in r[ISSUES]["media"]) # also validates ordinary fields - self.assertTrue('required' in r[ISSUES][self.test_field]) + self.assertTrue("required" in r[ISSUES][self.test_field]) r, s = self._post() self.assertEqual(STATUS_OK, r[STATUS]) @@ -61,10 +62,9 @@ def test_gridfs_media_storage_post(self): # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - returned = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + returned = r["_items"][0]["media"] # returned value is a base64 encoded string self.assertEqual(returned, self.encoded) @@ -74,29 +74,29 @@ def test_gridfs_media_storage_post(self): def test_gridfs_media_storage_post_excluded_file_in_result(self): # send something different than a file and get an error back - data = {'media': 'not a file'} + data = {"media": "not a file"} r, s = self.parse_response( - self.test_client.post(self.url, data=data, headers=self.headers)) + self.test_client.post(self.url, data=data, headers=self.headers) + ) self.assertEqual(STATUS_ERR, r[STATUS]) # validates media fields - self.assertTrue('must be of media type' in r[ISSUES]['media']) + self.assertTrue("must be of media type" in r[ISSUES]["media"]) # also validates ordinary fields - self.assertTrue('required' in r[ISSUES][self.test_field]) + self.assertTrue("required" in r[ISSUES][self.test_field]) r, s = self._post() self.assertEqual(STATUS_OK, r[STATUS]) - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False # compare original and returned data _id = r[self.id_field] # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - returned = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + returned = r["_items"][0]["media"] # returned value is a base64 encoded string self.assertEqual(returned, None) @@ -106,7 +106,7 @@ def test_gridfs_media_storage_post_extended(self): self.assertEqual(STATUS_OK, r[STATUS]) # request extended format file response - self.app.config['EXTENDED_MEDIA_INFO'] = ['content_type', 'length'] + self.app.config["EXTENDED_MEDIA_INFO"] = ["content_type", "length"] # compare original and returned data _id = r[self.id_field] @@ -114,45 +114,42 @@ def test_gridfs_media_storage_post_extended(self): # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - returned = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + returned = r["_items"][0]["media"] # returned value is a base64 encoded string - self.assertEqual(returned['file'], self.encoded) + self.assertEqual(returned["file"], self.encoded) # which decodes to the original clean - self.assertEqual(base64.decodestring(returned['file'].encode()), - self.clean) + self.assertEqual(base64.decodestring(returned["file"].encode()), self.clean) # also verify our extended fields - self.assertEqual(returned['content_type'], 'text/plain') - self.assertEqual(returned['length'], 16) + self.assertEqual(returned["content_type"], "text/plain") + self.assertEqual(returned["length"], 16) def test_gridfs_media_storage_post_extended_excluded_file_in_result(self): r, s = self._post() self.assertEqual(STATUS_OK, r[STATUS]) # request extended format file response - self.app.config['EXTENDED_MEDIA_INFO'] = ['content_type', 'length'] - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False + self.app.config["EXTENDED_MEDIA_INFO"] = ["content_type", "length"] + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False # compare original and returned data _id = r[self.id_field] # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - returned = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + returned = r["_items"][0]["media"] # returned value is None - self.assertEqual(returned['file'], None) + self.assertEqual(returned["file"], None) # also verify our extended fields - self.assertEqual(returned['content_type'], 'text/plain') - self.assertEqual(returned['length'], 16) + self.assertEqual(returned["content_type"], "text/plain") + self.assertEqual(returned["length"], 16) def test_gridfs_media_storage_put(self): r, s = self._post() @@ -167,15 +164,17 @@ def test_gridfs_media_storage_put(self): media_id = self.assertMediaStored(_id) # PUT replaces the file with new one - clean = b'my new file contents' + clean = b"my new file contents" encoded = base64.encodestring(clean).decode() - test_field, test_value = 'ref', "9234567890123456789054321" - data = {'media': (BytesIO(clean), 'test.txt'), test_field: test_value} - headers = [('Content-Type', 'multipart/form-data'), ('If-Match', etag)] + test_field, test_value = "ref", "9234567890123456789054321" + data = {"media": (BytesIO(clean), "test.txt"), test_field: test_value} + headers = [("Content-Type", "multipart/form-data"), ("If-Match", etag)] r, s = self.parse_response( - self.test_client.put(('%s/%s' % (self.url, _id)), data=data, - headers=headers)) + self.test_client.put( + ("%s/%s" % (self.url, _id)), data=data, headers=headers + ) + ) self.assertEqual(STATUS_OK, r[STATUS]) with self.app.test_request_context(): @@ -205,15 +204,17 @@ def test_gridfs_media_storage_patch(self): media_id = self.assertMediaStored(_id) # PATCH replaces the file with new one - clean = b'my new file contents' + clean = b"my new file contents" encoded = base64.encodestring(clean).decode() - test_field, test_value = 'ref', "9234567890123456789054321" - data = {'media': (BytesIO(clean), 'test.txt'), test_field: test_value} - headers = [('Content-Type', 'multipart/form-data'), ('If-Match', etag)] + test_field, test_value = "ref", "9234567890123456789054321" + data = {"media": (BytesIO(clean), "test.txt"), test_field: test_value} + headers = [("Content-Type", "multipart/form-data"), ("If-Match", etag)] r, s = self.parse_response( - self.test_client.patch(('%s/%s' % (self.url, _id)), data=data, - headers=headers)) + self.test_client.patch( + ("%s/%s" % (self.url, _id)), data=data, headers=headers + ) + ) self.assertEqual(STATUS_OK, r[STATUS]) # compare original and returned data @@ -228,7 +229,7 @@ def test_gridfs_media_storage_patch(self): def test_gridfs_media_storage_patch_null(self): # set 'media' field to 'nullable' - self.domain[self.known_resource]['schema']['media']['nullable'] = True + self.domain[self.known_resource]["schema"]["media"]["nullable"] = True response, status = self._post() self.assert201(status) @@ -237,15 +238,16 @@ def test_gridfs_media_storage_patch_null(self): etag = response[ETAG] # test that nullable media field can be set to None - data = {'media': None} - headers = [('If-Match', etag)] - response, status = self.patch(('%s/%s' % (self.url, _id)), data=data, - headers=headers) + data = {"media": None} + headers = [("If-Match", etag)] + response, status = self.patch( + ("%s/%s" % (self.url, _id)), data=data, headers=headers + ) self.assert200(status) response, status = self.get(self.known_resource, item=_id) self.assert200(status) - self.assertEqual(response['media'], None) + self.assertEqual(response["media"], None) def test_gridfs_media_storage_delete(self): r, s = self._post() @@ -259,11 +261,11 @@ def test_gridfs_media_storage_delete(self): media_id = self.assertMediaStored(_id) # DELETE deletes both the document and the media file - headers = [('If-Match', etag)] + headers = [("If-Match", etag)] r, s = self.parse_response( - self.test_client.delete(('%s/%s' % (self.url, _id)), - headers=headers)) + self.test_client.delete(("%s/%s" % (self.url, _id)), headers=headers) + ) self.assert204(s) with self.app.test_request_context(): @@ -271,8 +273,7 @@ def test_gridfs_media_storage_delete(self): self.assertFalse(self.app.media.exists(media_id, self.resource)) # GET returns 404 - r, s = self.parse_response(self.test_client.get('%s/%s' % (self.url, - _id))) + r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) self.assert404(s) def test_get_media_can_leverage_projection(self): @@ -285,39 +286,40 @@ def test_get_media_can_leverage_projection(self): _id = r[self.id_field] projection = '{"media": 1}' - response, status = self.parse_response(self.test_client.get( - '%s/%s?projection=%s' % - (self.resource_exclude_media_url, _id, projection)) + response, status = self.parse_response( + self.test_client.get( + "%s/%s?projection=%s" + % (self.resource_exclude_media_url, _id, projection) + ) ) self.assert200(status) - self.assertFalse('title' in response) - self.assertFalse('ref' in response) + self.assertFalse("title" in response) + self.assertFalse("ref" in response) # client-side projection should work - self.assertTrue('media' in response) - self.assertTrue(self.domain[self.known_resource]['id_field'] - in response) - self.assertTrue(self.app.config['ETAG'] in response) - self.assertTrue(self.app.config['LAST_UPDATED'] in response) - self.assertTrue(self.app.config['DATE_CREATED'] in response) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) - - response, status = self.parse_response(self.test_client.get( - '%s/%s' % (self.resource_exclude_media_url, _id))) + self.assertTrue("media" in response) + self.assertTrue(self.domain[self.known_resource]["id_field"] in response) + self.assertTrue(self.app.config["ETAG"] in response) + self.assertTrue(self.app.config["LAST_UPDATED"] in response) + self.assertTrue(self.app.config["DATE_CREATED"] in response) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) + + response, status = self.parse_response( + self.test_client.get("%s/%s" % (self.resource_exclude_media_url, _id)) + ) self.assert200(status) - self.assertTrue('title' in response) - self.assertTrue('ref' in response) + self.assertTrue("title" in response) + self.assertTrue("ref" in response) # not shown without projection - self.assertFalse('media' in response) - self.assertTrue(self.domain[self.known_resource]['id_field'] - in response) - self.assertTrue(self.app.config['ETAG'] in response) - self.assertTrue(self.app.config['LAST_UPDATED'] in response) - self.assertTrue(self.app.config['DATE_CREATED'] in response) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("media" in response) + self.assertTrue(self.domain[self.known_resource]["id_field"] in response) + self.assertTrue(self.app.config["ETAG"] in response) + self.assertTrue(self.app.config["LAST_UPDATED"] in response) + self.assertTrue(self.app.config["DATE_CREATED"] in response) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_gridfs_media_storage_delete_projection(self): """ test that #284 is fixed: If you have a media field, and set @@ -331,19 +333,17 @@ def test_gridfs_media_storage_delete_projection(self): # retrieve media_id and compare original and returned data media_id = self.assertMediaStored(_id) - self.app.config['DOMAIN']['contacts']['datasource']['projection'] = \ - {"media": 0} + self.app.config["DOMAIN"]["contacts"]["datasource"]["projection"] = {"media": 0} - r, s = self.parse_response(self.test_client.get('%s/%s' % (self.url, - _id))) + r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) etag = r[ETAG] # DELETE deletes both the document and the media file - headers = [('If-Match', etag)] + headers = [("If-Match", etag)] r, s = self.parse_response( - self.test_client.delete(('%s/%s' % (self.url, _id)), - headers=headers)) + self.test_client.delete(("%s/%s" % (self.url, _id)), headers=headers) + ) self.assert204(s) with self.app.test_request_context(): @@ -351,14 +351,13 @@ def test_gridfs_media_storage_delete_projection(self): self.assertFalse(self.app.media.exists(media_id, self.resource)) # GET returns 404 - r, s = self.parse_response(self.test_client.get('%s/%s' % (self.url, - _id))) + r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) self.assert404(s) def test_gridfs_media_storage_return_url(self): self.app._init_media_endpoint() - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False - self.app.config['RETURN_MEDIA_AS_URL'] = True + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False + self.app.config["RETURN_MEDIA_AS_URL"] = True r, s = self._post() self.assertEqual(STATUS_OK, r[STATUS]) @@ -366,46 +365,44 @@ def test_gridfs_media_storage_return_url(self): # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - url = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + url = r["_items"][0]["media"] with self.app.test_request_context(): media_id = self.assertMediaStored(_id) - self.assertEqual('/media/%s' % media_id, url) + self.assertEqual("/media/%s" % media_id, url) response = self.test_client.get(url) self.assertEqual(self.clean, response.get_data()) def test_gridfs_partial_media(self): self.app._init_media_endpoint() - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False - self.app.config['RETURN_MEDIA_AS_URL'] = True + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False + self.app.config["RETURN_MEDIA_AS_URL"] = True r, s = self._post() _id = r[self.id_field] where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - url = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + url = r["_items"][0]["media"] - headers = {'Range': 'bytes=0-5'} + headers = {"Range": "bytes=0-5"} response = self.test_client.get(url, headers=headers) self.assertEqual(self.clean[:6], response.get_data()) - headers = {'Range': 'bytes=5-10'} + headers = {"Range": "bytes=5-10"} response = self.test_client.get(url, headers=headers) self.assertEqual(self.clean[5:11], response.get_data()) - headers = {'Range': 'bytes=0-999'} + headers = {"Range": "bytes=0-999"} response = self.test_client.get(url, headers=headers) self.assertEqual(self.clean, response.get_data()) def test_gridfs_media_storage_base_url(self): self.app._init_media_endpoint() - self.app.config['RETURN_MEDIA_AS_BASE64_STRING'] = False - self.app.config['RETURN_MEDIA_AS_URL'] = True - self.app.config['MEDIA_BASE_URL'] = 'http://s3-us-west-2.amazonaws.com' - self.app.config['MEDIA_ENDPOINT'] = 'foo' + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False + self.app.config["RETURN_MEDIA_AS_URL"] = True + self.app.config["MEDIA_BASE_URL"] = "http://s3-us-west-2.amazonaws.com" + self.app.config["MEDIA_ENDPOINT"] = "foo" r, s = self._post() self.assertEqual(STATUS_OK, r[STATUS]) @@ -413,21 +410,26 @@ def test_gridfs_media_storage_base_url(self): # GET the file at the resource endpoint where = 'where={"%s": "%s"}' % (self.id_field, _id) - r, s = self.parse_response( - self.test_client.get('%s?%s' % (self.url, where))) - self.assertEqual(len(r['_items']), 1) - url = r['_items'][0]['media'] + r, s = self.parse_response(self.test_client.get("%s?%s" % (self.url, where))) + self.assertEqual(len(r["_items"]), 1) + url = r["_items"][0]["media"] with self.app.test_request_context(): media_id = self.assertMediaStored(_id) - self.assertEqual('%s/%s/%s' % (self.app.config['MEDIA_BASE_URL'], - self.app.config['MEDIA_ENDPOINT'], media_id), url) + self.assertEqual( + "%s/%s/%s" + % ( + self.app.config["MEDIA_BASE_URL"], + self.app.config["MEDIA_ENDPOINT"], + media_id, + ), + url, + ) def assertMediaField(self, _id, encoded, clean): # GET the file at the item endpoint - r, s = self.parse_response(self.test_client.get('%s/%s' % (self.url, - _id))) - returned = r['media'] + r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) + returned = r["media"] # returned value is a base64 encoded string self.assertEqual(returned, encoded) # which decodes to the original file clean @@ -436,9 +438,8 @@ def assertMediaField(self, _id, encoded, clean): def assertMediaFieldExtended(self, _id, encoded, clean): # GET the file at the item endpoint - r, s = self.parse_response(self.test_client.get('%s/%s' % (self.url, - _id))) - returned = r['media']['file'] + r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) + returned = r["media"]["file"] # returned value is a base64 encoded string self.assertEqual(returned, encoded) # which decodes to the original file clean @@ -449,8 +450,7 @@ def assertMediaStored(self, _id): _db = self.connection[MONGO_DBNAME] # retrieve media id - media_id = _db.contacts.find_one( - {self.id_field: ObjectId(_id)})['media'] + media_id = _db.contacts.find_one({self.id_field: ObjectId(_id)})["media"] # verify it's actually stored in the media storage system self.assertTrue(self.app.media.exists(media_id, self.resource)) @@ -458,14 +458,22 @@ def assertMediaStored(self, _id): def _post(self): # send a file and a required, ordinary field with no issues - data = {'media': (BytesIO(self.clean), 'test.txt'), self.test_field: - self.test_value} - return self.parse_response(self.test_client.post( - self.url, data=data, headers=self.headers)) + data = { + "media": (BytesIO(self.clean), "test.txt"), + self.test_field: self.test_value, + } + return self.parse_response( + self.test_client.post(self.url, data=data, headers=self.headers) + ) def _post_hide_media(self): # send a file and a required, ordinary field with no issues - data = {'media': (BytesIO(self.clean), 'test.txt'), self.test_field: - self.test_value} - return self.parse_response(self.test_client.post( - self.resource_exclude_media_url, data=data, headers=self.headers)) + data = { + "media": (BytesIO(self.clean), "test.txt"), + self.test_field: self.test_value, + } + return self.parse_response( + self.test_client.post( + self.resource_exclude_media_url, data=data, headers=self.headers + ) + ) diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index 30bfba6b5..3e6fed495 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -14,73 +14,71 @@ class TestPythonParser(TestCase): - def test_Eq(self): r = parse('a == "whatever"') self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': 'whatever'}) + self.assertEqual(r, {"a": "whatever"}) def test_Gt(self): - r = parse('a > 1') + r = parse("a > 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': {'$gt': 1}}) + self.assertEqual(r, {"a": {"$gt": 1}}) def test_GtE(self): - r = parse('a >= 1') + r = parse("a >= 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': {'$gte': 1}}) + self.assertEqual(r, {"a": {"$gte": 1}}) def test_Lt(self): - r = parse('a < 1') + r = parse("a < 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': {'$lt': 1}}) + self.assertEqual(r, {"a": {"$lt": 1}}) def test_LtE(self): - r = parse('a <= 1') + r = parse("a <= 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': {'$lte': 1}}) + self.assertEqual(r, {"a": {"$lte": 1}}) def test_NotEq(self): - r = parse('a != 1') + r = parse("a != 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'a': {'$ne': 1}}) + self.assertEqual(r, {"a": {"$ne": 1}}) def test_And_BoolOp(self): - r = parse('a == 1 and b == 2') + r = parse("a == 1 and b == 2") self.assertEqual(type(r), dict) - self.assertEqual(r, {'$and': [{'a': 1}, {'b': 2}]}) + self.assertEqual(r, {"$and": [{"a": 1}, {"b": 2}]}) def test_Or_BoolOp(self): - r = parse('a == 1 or b == 2') + r = parse("a == 1 or b == 2") self.assertEqual(type(r), dict) - self.assertEqual(r, {'$or': [{'a': 1}, {'b': 2}]}) + self.assertEqual(r, {"$or": [{"a": 1}, {"b": 2}]}) def test_nested_BoolOp(self): - r = parse('a == 1 or (b == 2 and c == 3)') + r = parse("a == 1 or (b == 2 and c == 3)") self.assertEqual(type(r), dict) - self.assertEqual(r, {'$or': [{'a': 1}, - {'$and': [{'b': 2}, {'c': 3}]}]}) + self.assertEqual(r, {"$or": [{"a": 1}, {"$and": [{"b": 2}, {"c": 3}]}]}) def test_ObjectId_Call(self): r = parse('_id == ObjectId("4f4644fbc88e20212c000000")') self.assertEqual(type(r), dict) - self.assertEqual(r, {'_id': ObjectId("4f4644fbc88e20212c000000")}) + self.assertEqual(r, {"_id": ObjectId("4f4644fbc88e20212c000000")}) def test_datetime_Call(self): - r = parse('born == datetime(2012, 11, 9)') + r = parse("born == datetime(2012, 11, 9)") self.assertEqual(type(r), dict) - self.assertEqual(r, {'born': datetime(2012, 11, 9)}) + self.assertEqual(r, {"born": datetime(2012, 11, 9)}) def test_Attribute(self): - r = parse('Invoice.number == 1') + r = parse("Invoice.number == 1") self.assertEqual(type(r), dict) - self.assertEqual(r, {'Invoice.number': 1}) + self.assertEqual(r, {"Invoice.number": 1}) def test_unparsed_statement(self): self.assertRaises(ParseError, parse, 'print ("hello")') def test_bad_Expr(self): - self.assertRaises(ParseError, parse, 'a | 2') + self.assertRaises(ParseError, parse, "a | 2") class TestMongoValidator(TestCase): @@ -95,296 +93,350 @@ def test_unique_success(self): pass def test_decimal_fail(self): - schema = {'decimal': {'type': 'decimal'}} - doc = {'decimal': 'not_a_decimal'} + schema = {"decimal": {"type": "decimal"}} + doc = {"decimal": "not_a_decimal"} v = Validator(schema, None) self.assertFalse(v.validate(doc)) - self.assertTrue('decimal' in v.errors) - self.assertTrue('decimal' in v.errors['decimal']) + self.assertTrue("decimal" in v.errors) + self.assertTrue("decimal" in v.errors["decimal"]) def test_decimal_success(self): - schema = {'decimal': {'type': 'decimal'}} - doc = {'decimal': decimal128.Decimal128('123.123')} + schema = {"decimal": {"type": "decimal"}} + doc = {"decimal": decimal128.Decimal128("123.123")} v = Validator(schema, None) self.assertTrue(v.validate(doc)) def test_objectid_fail(self): - schema = {'id': {'type': 'objectid'}} - doc = {'id': 'not_an_object_id'} + schema = {"id": {"type": "objectid"}} + doc = {"id": "not_an_object_id"} v = Validator(schema, None) self.assertFalse(v.validate(doc)) - self.assertTrue('id' in v.errors) - self.assertTrue('objectid' in v.errors['id']) + self.assertTrue("id" in v.errors) + self.assertTrue("objectid" in v.errors["id"]) def test_objectid_success(self): - schema = {'id': {'type': 'objectid'}} - doc = {'id': ObjectId('50656e4538345b39dd0414f0')} + schema = {"id": {"type": "objectid"}} + doc = {"id": ObjectId("50656e4538345b39dd0414f0")} v = Validator(schema, None) self.assertTrue(v.validate(doc)) def test_dbref_fail(self): - schema = {'id': {'type': 'dbref'}} - doc = {'id': 'not_an_object_id'} + schema = {"id": {"type": "dbref"}} + doc = {"id": "not_an_object_id"} v = Validator(schema, None) self.assertFalse(v.validate(doc)) - self.assertTrue('id' in v.errors) - self.assertTrue('dbref' in v.errors['id']) + self.assertTrue("id" in v.errors) + self.assertTrue("dbref" in v.errors["id"]) def test_dbref_success(self): - schema = {'id': {'type': 'dbref'}} - doc = {'id': DBRef("SomeCollection", - ObjectId("50656e4538345b39dd0414f0"))} + schema = {"id": {"type": "dbref"}} + doc = {"id": DBRef("SomeCollection", ObjectId("50656e4538345b39dd0414f0"))} v = Validator(schema, None) self.assertTrue(v.validate(doc)) def test_reject_invalid_schema(self): - schema = {'a_field': {'foo': 'bar'}} + schema = {"a_field": {"foo": "bar"}} self.assertRaises(SchemaError, lambda: Validator(schema)) def test_geojson_not_compilant(self): - schema = {'location': {'type': 'point'}} - doc = {'location': [10.0, 123.0]} + schema = {"location": {"type": "point"}} + doc = {"location": [10.0, 123.0]} v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('point' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("point" in v.errors["location"]) def test_geometry_not_compilant(self): - schema = {'location': {'type': 'point'}} - doc = {'location': {"type": "Point", "geometries": [10.0, 123.0]}} + schema = {"location": {"type": "point"}} + doc = {"location": {"type": "Point", "geometries": [10.0, 123.0]}} v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('point' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("point" in v.errors["location"]) def test_geometrycollection_not_compilant(self): - schema = {'location': {'type': 'geometrycollection'}} - doc = {'location': {"type": "GeometryCollection", - "coordinates": [10.0, 123.0]}} + schema = {"location": {"type": "geometrycollection"}} + doc = {"location": {"type": "GeometryCollection", "coordinates": [10.0, 123.0]}} v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('geometrycollection' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("geometrycollection" in v.errors["location"]) def test_point_success(self): - schema = {'location': {'type': 'point'}} - doc = {'location': {"type": "Point", "coordinates": [100.0, 0.0]}} + schema = {"location": {"type": "point"}} + doc = {"location": {"type": "Point", "coordinates": [100.0, 0.0]}} v = Validator(schema) self.assertTrue(v.validate(doc)) def test_point_fail(self): - schema = {'location': {'type': 'point'}} - doc = {'location': {'type': "Point", 'coordinates': ["asdasd", 123.0]}} + schema = {"location": {"type": "point"}} + doc = {"location": {"type": "Point", "coordinates": ["asdasd", 123.0]}} v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('point' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("point" in v.errors["location"]) def test_point_coordinates_fail(self): - schema = {'location': {'type': 'point'}} - doc = {'location': {'type': "Point", 'coordinates': [123.0]}} + schema = {"location": {"type": "point"}} + doc = {"location": {"type": "Point", "coordinates": [123.0]}} v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('point' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("point" in v.errors["location"]) def test_point_integer_success(self): - schema = {'location': {'type': 'point'}} - doc = {'location': {'type': "Point", 'coordinates': [10, 123.0]}} + schema = {"location": {"type": "point"}} + doc = {"location": {"type": "Point", "coordinates": [10, 123.0]}} v = Validator(schema) self.assertTrue(v.validate(doc)) def test_linestring_success(self): - schema = {'location': {'type': 'linestring'}} - doc = {'location': {"type": "LineString", - "coordinates": [[100.0, 0.0], [101.0, 1.0]] - }} + schema = {"location": {"type": "linestring"}} + doc = { + "location": { + "type": "LineString", + "coordinates": [[100.0, 0.0], [101.0, 1.0]], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_linestring_fail(self): - schema = {'location': {'type': 'linestring'}} - doc = {'location': {'type': "LineString", - 'coordinates': [[12.0, 123.0], [12, 'eve']]}} + schema = {"location": {"type": "linestring"}} + doc = { + "location": { + "type": "LineString", + "coordinates": [[12.0, 123.0], [12, "eve"]], + } + } v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('linestring' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("linestring" in v.errors["location"]) def test_polygon_success(self): - schema = {'location': {'type': 'polygon'}} - doc = {'location': {"type": "Polygon", - "coordinates": [[[100.0, 0.0], [101.0, 0.0], - [101.0, 1.0], [100.0, 1.0], - [100.0, 0.0]] - ] - } - } + schema = {"location": {"type": "polygon"}} + doc = { + "location": { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ] + ], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_polygon_fail(self): - schema = {'location': {'type': 'polygon'}} - doc = {'location': {'type': "Polygon", - 'coordinates': [[[12.0, 23.0], [12.3, 12.5]], - ["eve"]]}} + schema = {"location": {"type": "polygon"}} + doc = { + "location": { + "type": "Polygon", + "coordinates": [[[12.0, 23.0], [12.3, 12.5]], ["eve"]], + } + } v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('location' in v.errors) - self.assertTrue('polygon' in v.errors['location']) + self.assertTrue("location" in v.errors) + self.assertTrue("polygon" in v.errors["location"]) def test_multipoint_success(self): - schema = {'location': {'type': 'multipoint'}} - doc = {'location': {"type": "MultiPoint", - "coordinates": [[100.0, 0.0], [101.0, 1.0]] - } - } + schema = {"location": {"type": "multipoint"}} + doc = { + "location": { + "type": "MultiPoint", + "coordinates": [[100.0, 0.0], [101.0, 1.0]], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_multilinestring_success(self): - schema = {'location': {'type': 'multilinestring'}} - doc = {'location': {"type": "MultiLineString", - "coordinates": [[[100.0, 0.0], [101.0, 1.0]], - [[102.0, 2.0], [103.0, 3.0]] - ] - } - } + schema = {"location": {"type": "multilinestring"}} + doc = { + "location": { + "type": "MultiLineString", + "coordinates": [ + [[100.0, 0.0], [101.0, 1.0]], + [[102.0, 2.0], [103.0, 3.0]], + ], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_multipolygon_success(self): - schema = {'location': {'type': 'multipolygon'}} - doc = {'location': {"type": "MultiPolygon", - "coordinates": [[[[102.0, 2.0], [103.0, 2.0], - [103.0, 3.0], [102.0, 3.0], - [102.0, 2.0]]], - [[[100.0, 0.0], [101.0, 0.0], - [101.0, 1.0], [100.0, 1.0], - [100.0, 0.0]], - [[100.2, 0.2], [100.8, 0.2], - [100.8, 0.8], [100.2, 0.8], - [100.2, 0.2]]] - ] - } - } + schema = {"location": {"type": "multipolygon"}} + doc = { + "location": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [102.0, 2.0], + [103.0, 2.0], + [103.0, 3.0], + [102.0, 3.0], + [102.0, 2.0], + ] + ], + [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ], + [ + [100.2, 0.2], + [100.8, 0.2], + [100.8, 0.8], + [100.2, 0.8], + [100.2, 0.2], + ], + ], + ], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_geometrycollection_success(self): - schema = {'locations': {'type': 'geometrycollection'}} - doc = {'locations': {'type': "GeometryCollection", - "geometries": [{"type": "Point", - "coordinates": [100.0, 0.0]}, - {"type": "LineString", - "coordinates": [[101.0, 0.0], - [102.0, 1.0]] - } - ] - } - } + schema = {"locations": {"type": "geometrycollection"}} + doc = { + "locations": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [100.0, 0.0]}, + {"type": "LineString", "coordinates": [[101.0, 0.0], [102.0, 1.0]]}, + ], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_geometrycollection_fail(self): - schema = {'locations': {'type': 'geometrycollection'}} - doc = {'locations': {'type': "GeometryCollection", - "geometries": [{"type": "GeoJSON", - "badinput": "lolololololol"}] - } - } + schema = {"locations": {"type": "geometrycollection"}} + doc = { + "locations": { + "type": "GeometryCollection", + "geometries": [{"type": "GeoJSON", "badinput": "lolololololol"}], + } + } v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('locations' in v.errors) - self.assertTrue('geometrycollection' in v.errors['locations']) + self.assertTrue("locations" in v.errors) + self.assertTrue("geometrycollection" in v.errors["locations"]) def test_feature_success(self): - schema = {'locations': {'type': 'feature'}} - doc = {"locations": {"type": "Feature", - "geometry": {"type": "Polygon", - "coordinates": [[[100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0]]]} - } - } + schema = {"locations": {"type": "feature"}} + doc = { + "locations": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ] + ], + }, + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_feature_fail(self): - schema = {'locations': {'type': 'feature'}} - doc = {"locations": {"type": "Feature", - "geometries": [{"type": "Polygon", - "coordinates": [[[100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 0.0]]]}] - } - } + schema = {"locations": {"type": "feature"}} + doc = { + "locations": { + "type": "Feature", + "geometries": [ + { + "type": "Polygon", + "coordinates": [ + [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 0.0]] + ], + } + ], + } + } v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('locations' in v.errors) - self.assertTrue('feature' in v.errors['locations']) + self.assertTrue("locations" in v.errors) + self.assertTrue("feature" in v.errors["locations"]) def test_featurecollection_success(self): - schema = {'locations': {'type': 'featurecollection'}} - doc = {"locations": {"type": "FeatureCollection", - "features": [ - {"type": "Feature", - "geometry": {"type": "Point", - "coordinates": [102.0, 0.5]} - }] - } - } + schema = {"locations": {"type": "featurecollection"}} + doc = { + "locations": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, + } + ], + } + } v = Validator(schema) self.assertTrue(v.validate(doc)) def test_featurecollection_fail(self): - schema = {'locations': {'type': 'featurecollection'}} - doc = {"locations": {"type": "FeatureCollection", - "geometry": {"type": "Point", - "coordinates": [100.0, 0.0]} - } - } + schema = {"locations": {"type": "featurecollection"}} + doc = { + "locations": { + "type": "FeatureCollection", + "geometry": {"type": "Point", "coordinates": [100.0, 0.0]}, + } + } v = Validator(schema) self.assertFalse(v.validate(doc)) - self.assertTrue('locations' in v.errors) - self.assertTrue('featurecollection' in v.errors['locations']) + self.assertTrue("locations" in v.errors) + self.assertTrue("featurecollection" in v.errors["locations"]) def test_dependencies_with_defaults(self): schema = { - 'test_field': {'dependencies': 'foo'}, - 'foo': {'type': 'string', 'default': 'foo'}, - 'bar': {'type': 'string', 'default': 'bar'} + "test_field": {"dependencies": "foo"}, + "foo": {"type": "string", "default": "foo"}, + "bar": {"type": "string", "default": "bar"}, } - doc = {'test_field': 'foobar'} + doc = {"test_field": "foobar"} # With `dependencies` as a str v = Validator(schema) self.assertTrue(v.validate(doc)) # With `dependencies` as a dict - schema['test_field'] = {'dependencies': {'foo': 'foo', 'bar': 'bar'}} + schema["test_field"] = {"dependencies": {"foo": "foo", "bar": "bar"}} v = Validator(schema) self.assertTrue(v.validate(doc)) # With `dependencies` as a list - schema['test_field'] = {'dependencies': ['foo', 'bar']} + schema["test_field"] = {"dependencies": ["foo", "bar"]} v = Validator(schema) self.assertTrue(v.validate(doc)) class TestMongoDriver(TestBase): - def test_combine_queries(self): mongo = Mongo(None) - query_a = {'username': {'$exists': True}} - query_b = {'username': 'mike'} + query_a = {"username": {"$exists": True}} + query_b = {"username": "mike"} combined = mongo.combine_queries(query_a, query_b) self.assertEqual( - combined, - {'$and': [{'username': {'$exists': True}}, {'username': 'mike'}]} + combined, {"$and": [{"username": {"$exists": True}}, {"username": "mike"}]} ) def test_json_encoder_class(self): @@ -394,29 +446,34 @@ def test_json_encoder_class(self): def test_get_value_from_query(self): mongo = Mongo(None) - simple_query = {'_id': 'abcdef012345678901234567'} - compound_query = {'$and': [ - {'username': {'$exists': False}}, - {'_id': 'abcdef012345678901234567'} - ]} - self.assertEqual(mongo.get_value_from_query(simple_query, '_id'), - 'abcdef012345678901234567') - self.assertEqual(mongo.get_value_from_query(compound_query, '_id'), - 'abcdef012345678901234567') + simple_query = {"_id": "abcdef012345678901234567"} + compound_query = { + "$and": [ + {"username": {"$exists": False}}, + {"_id": "abcdef012345678901234567"}, + ] + } + self.assertEqual( + mongo.get_value_from_query(simple_query, "_id"), "abcdef012345678901234567" + ) + self.assertEqual( + mongo.get_value_from_query(compound_query, "_id"), + "abcdef012345678901234567", + ) def test_query_contains_field(self): mongo = Mongo(None) - simple_query = {'_id': 'abcdef012345678901234567'} - compound_query = {'$and': [ - {'username': {'$exists': False}}, - {'_id': 'abcdef012345678901234567'} - ]} - self.assertTrue(mongo.query_contains_field(simple_query, '_id')) - self.assertFalse(mongo.query_contains_field(simple_query, - 'fake-field')) - self.assertTrue(mongo.query_contains_field(compound_query, '_id')) - self.assertFalse(mongo.query_contains_field(compound_query, - 'fake-field')) + simple_query = {"_id": "abcdef012345678901234567"} + compound_query = { + "$and": [ + {"username": {"$exists": False}}, + {"_id": "abcdef012345678901234567"}, + ] + } + self.assertTrue(mongo.query_contains_field(simple_query, "_id")) + self.assertFalse(mongo.query_contains_field(simple_query, "fake-field")) + self.assertTrue(mongo.query_contains_field(compound_query, "_id")) + self.assertFalse(mongo.query_contains_field(compound_query, "fake-field")) def test_delete_returns_status(self): db = self.connection[MONGO_DBNAME] diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index b1cb45286..a6925c9b5 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -9,9 +9,14 @@ import eve from eve.auth import BasicAuth from eve.tests import TestBase -from eve.tests.test_settings import MONGO1_PASSWORD, MONGO1_USERNAME, \ - MONGO1_DBNAME, MONGO_DBNAME, \ - MONGO_HOST, MONGO_PORT +from eve.tests.test_settings import ( + MONGO1_PASSWORD, + MONGO1_USERNAME, + MONGO1_DBNAME, + MONGO_DBNAME, + MONGO_HOST, + MONGO_PORT, +) class TestMultiMongo(TestBase): @@ -20,16 +25,10 @@ def setUp(self): self.setupDB2() - schema = { - 'author': {'type': 'string'}, - 'title': {'type': 'string'}, - } - settings = { - 'schema': schema, - 'mongo_prefix': 'MONGO1' - } + schema = {"author": {"type": "string"}, "title": {"type": "string"}} + settings = {"schema": schema, "mongo_prefix": "MONGO1"} - self.app.register_resource('works', settings) + self.app.register_resource("works", settings) def tearDown(self): super(TestMultiMongo, self).tearDown() @@ -40,11 +39,12 @@ def setupDB2(self): self.connection.drop_database(MONGO1_DBNAME) db = self.connection[MONGO1_DBNAME] try: - db.command('dropUser', MONGO1_USERNAME) + db.command("dropUser", MONGO1_USERNAME) except OperationFailure: pass - db.command('createUser', MONGO1_USERNAME, pwd=MONGO1_PASSWORD, - roles=['dbAdmin']) + db.command( + "createUser", MONGO1_USERNAME, pwd=MONGO1_PASSWORD, roles=["dbAdmin"] + ) self.bulk_insert2() def dropDB2(self): @@ -64,8 +64,8 @@ def random_works(self, num): for i in range(num): dt = datetime.now() work = { - 'author': self.random_string(20), - 'title': self.random_string(30), + "author": self.random_string(20), + "title": self.random_string(30), eve.LAST_UPDATED: dt, eve.DATE_CREATED: dt, } @@ -76,13 +76,13 @@ def random_works(self, num): class TestMethodsAcrossMultiMongo(TestMultiMongo): def test_get_multidb(self): # test that a GET on 'works' reads from MONGO1 - id_field = self.domain['works']['id_field'] - r, s = self.get('works/%s' % self.work[id_field]) + id_field = self.domain["works"]["id_field"] + r, s = self.get("works/%s" % self.work[id_field]) self.assert200(s) - self.assertEqual(r['author'], self.work['author']) + self.assertEqual(r["author"], self.work["author"]) # while 'contacts' endpoint reads from MONGO - id_field = self.domain['contacts']['id_field'] + id_field = self.domain["contacts"]["id_field"] r, s = self.get(self.known_resource, item=self.item_id) self.assert200(s) self.assertEqual(r[id_field], self.item_id) @@ -91,17 +91,17 @@ def test_post_multidb(self): # test that a POST on 'works' stores data to MONGO1 work = self._save_work() db = self.connection[MONGO1_DBNAME] - id_field = self.domain['works']['id_field'] + id_field = self.domain["works"]["id_field"] new = db.works.find_one({id_field: ObjectId(work[id_field])}) self.assertTrue(new is not None) self.connection.close() # while 'contacts' endpoint stores data to MONGO - contact = {'ref': '1234567890123456789054321'} + contact = {"ref": "1234567890123456789054321"} r, s = self.post(self.known_resource_url, data=contact) self.assert201(s) db = self.connection[MONGO_DBNAME] - id_field = self.domain['contacts']['id_field'] + id_field = self.domain["contacts"]["id_field"] new = db.contacts.find_one({id_field: ObjectId(r[id_field])}) self.assertTrue(new is not None) self.connection.close() @@ -109,28 +109,29 @@ def test_post_multidb(self): def test_patch_multidb(self): # test that a PATCH on 'works' udpates data on MONGO1 work = self._save_work() - id_field = self.domain['works']['id_field'] + id_field = self.domain["works"]["id_field"] id, etag = work[id_field], work[eve.ETAG] - changes = {'author': 'mike'} + changes = {"author": "mike"} - headers = [('Content-Type', 'application/json'), ('If-Match', etag)] - r = self.test_client.patch('works/%s' % id, data=json.dumps(changes), - headers=headers) + headers = [("Content-Type", "application/json"), ("If-Match", etag)] + r = self.test_client.patch( + "works/%s" % id, data=json.dumps(changes), headers=headers + ) self.assert200(r.status_code) db = self.connection[MONGO1_DBNAME] updated = db.works.find_one({id_field: ObjectId(id)}) - self.assertEqual(updated['author'], 'mike') + self.assertEqual(updated["author"], "mike") self.connection.close() # while 'contacts' endpoint updates data on MONGO field, value = "ref", "1234567890123456789012345" changes = {field: value} - headers = [('Content-Type', 'application/json'), ('If-Match', - self.item_etag)] - id_field = self.domain['contacts']['id_field'] - r = self.test_client.patch(self.item_id_url, data=json.dumps(changes), - headers=headers) + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] + id_field = self.domain["contacts"]["id_field"] + r = self.test_client.patch( + self.item_id_url, data=json.dumps(changes), headers=headers + ) self.assert200(r.status_code) db = self.connection[MONGO_DBNAME] @@ -141,28 +142,29 @@ def test_patch_multidb(self): def test_put_multidb(self): # test that a PUT on 'works' udpates data on MONGO1 work = self._save_work() - id_field = self.domain['works']['id_field'] + id_field = self.domain["works"]["id_field"] id, etag = work[id_field], work[eve.ETAG] - changes = {'author': 'mike', 'title': 'Eve for dummies'} + changes = {"author": "mike", "title": "Eve for dummies"} - headers = [('Content-Type', 'application/json'), ('If-Match', etag)] - r = self.test_client.put('works/%s' % id, data=json.dumps(changes), - headers=headers) + headers = [("Content-Type", "application/json"), ("If-Match", etag)] + r = self.test_client.put( + "works/%s" % id, data=json.dumps(changes), headers=headers + ) self.assert200(r.status_code) db = self.connection[MONGO1_DBNAME] updated = db.works.find_one({id_field: ObjectId(id)}) - self.assertEqual(updated['author'], 'mike') + self.assertEqual(updated["author"], "mike") self.connection.close() # while 'contacts' endpoint updates data on MONGO field, value = "ref", "1234567890123456789012345" changes = {field: value} - headers = [('Content-Type', 'application/json'), ('If-Match', - self.item_etag)] - id_field = self.domain['contacts']['id_field'] - r = self.test_client.put(self.item_id_url, data=json.dumps(changes), - headers=headers) + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] + id_field = self.domain["contacts"]["id_field"] + r = self.test_client.put( + self.item_id_url, data=json.dumps(changes), headers=headers + ) self.assert200(r.status_code) db = self.connection[MONGO_DBNAME] @@ -173,10 +175,9 @@ def test_put_multidb(self): def test_delete_multidb(self): # test that DELETE on 'works' deletes data on MONGO1 work = self._save_work() - id_field = self.domain['works']['id_field'] + id_field = self.domain["works"]["id_field"] id, etag = work[id_field], work[eve.ETAG] - r = self.test_client.delete('works/%s' % id, headers=[('If-Match', - etag)]) + r = self.test_client.delete("works/%s" % id, headers=[("If-Match", etag)]) self.assert204(r.status_code) db = self.connection[MONGO1_DBNAME] lost = db.works.find_one({id_field: ObjectId(id)}) @@ -184,83 +185,89 @@ def test_delete_multidb(self): self.connection.close() # while 'contacts' still deletes on MONGO - r = self.test_client.delete(self.item_id_url, - headers=[('If-Match', self.item_etag)]) + r = self.test_client.delete( + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(r.status_code) db = self.connection[MONGO_DBNAME] - id_field = self.domain['contacts']['id_field'] + id_field = self.domain["contacts"]["id_field"] lost = db.contacts.find_one({id_field: ObjectId(self.item_id)}) self.assertEqual(lost, None) self.connection.close() def test_create_index_with_mongo_uri_and_prefix(self): - self.app.config['MONGO_URI'] = 'mongodb://%s:%s/%s' % ( - MONGO_HOST, MONGO_PORT, MONGO_DBNAME) - self.app.config['MONGO1_URI'] = 'mongodb://%s:%s/%s' % ( - MONGO_HOST, MONGO_PORT, MONGO1_DBNAME) + self.app.config["MONGO_URI"] = "mongodb://%s:%s/%s" % ( + MONGO_HOST, + MONGO_PORT, + MONGO_DBNAME, + ) + self.app.config["MONGO1_URI"] = "mongodb://%s:%s/%s" % ( + MONGO_HOST, + MONGO_PORT, + MONGO1_DBNAME, + ) settings = { - 'schema': { - 'name': {'type': 'string'}, - 'other_field': {'type': 'string'}, - 'lat_long': {'type': 'list'} + "schema": { + "name": {"type": "string"}, + "other_field": {"type": "string"}, + "lat_long": {"type": "list"}, }, - 'mongo_indexes': { - 'name': [('name', 1)], - 'composed': [('name', 1), ('other_field', 1)], - 'arguments': ([('lat_long', "2d")], {"sparse": True}) + "mongo_indexes": { + "name": [("name", 1)], + "composed": [("name", 1), ("other_field", 1)], + "arguments": ([("lat_long", "2d")], {"sparse": True}), }, - 'mongo_prefix': 'MONGO1', + "mongo_prefix": "MONGO1", } - self.app.register_resource('mongodb_features', settings) + self.app.register_resource("mongodb_features", settings) # check if index was created using MONGO1 prefix db = self.connection[MONGO1_DBNAME] - self.assertTrue('mongodb_features' in db.collection_names()) - coll = db['mongodb_features'] + self.assertTrue("mongodb_features" in db.collection_names()) + coll = db["mongodb_features"] indexes = coll.index_information() # at least there is an index for the _id field plus the indexes - self.assertTrue(len(indexes) > len(settings['mongo_indexes'])) + self.assertTrue(len(indexes) > len(settings["mongo_indexes"])) def _save_work(self): - work = {'author': 'john doe', 'title': 'Eve for Dummies'} - r, s = self.post('works', data=work) + work = {"author": "john doe", "title": "Eve for Dummies"} + r, s = self.post("works", data=work) self.assert201(s) return r class MyBasicAuth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): - self.set_mongo_prefix('MONGO1') + self.set_mongo_prefix("MONGO1") return True class TestMultiMongoAuth(TestMultiMongo): def test_get_multidb(self): - self.domain['works']['mongo_prefix'] = 'MONGO' - self.domain['works']['public_item_methods'] = [] + self.domain["works"]["mongo_prefix"] = "MONGO" + self.domain["works"]["public_item_methods"] = [] - headers = [('Authorization', 'Basic YWRtaW46c2VjcmV0')] + headers = [("Authorization", "Basic YWRtaW46c2VjcmV0")] # this will 404 since there's no 'works' collection on MONGO, - id_field = self.domain['works']['id_field'] - r = self.test_client.get('works/%s' % self.work[id_field], - headers=headers) + id_field = self.domain["works"]["id_field"] + r = self.test_client.get("works/%s" % self.work[id_field], headers=headers) self.assert404(r.status_code) # now set a custom auth class which sets mongo_prefix at MONGO1 - self.domain['works']['authentication'] = MyBasicAuth + self.domain["works"]["authentication"] = MyBasicAuth # this will 200 just fine as the custom auth class has precedence over # endpoint configuration. - r = self.test_client.get('works/%s' % self.work[id_field], - headers=headers) + r = self.test_client.get("works/%s" % self.work[id_field], headers=headers) self.assert200(r.status_code) # test that we are indeed reading from the correct database instance. - payl = json.loads(r.get_data().decode('utf-8')) - self.assertEqual(payl['author'], self.work['author']) + payl = json.loads(r.get_data().decode("utf-8")) + self.assertEqual(payl["author"], self.work["author"]) # 'contacts' still reads from MONGO - r = self.test_client.get('%s/%s' % (self.known_resource_url, - self.item_id), headers=headers) + r = self.test_client.get( + "%s/%s" % (self.known_resource_url, self.item_id), headers=headers + ) self.assert200(r.status_code) diff --git a/eve/tests/logging.py b/eve/tests/logging.py index 8a6f21fac..fd32fffd2 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -11,10 +11,8 @@ class TestUtils(TestBase): @log_capture() def test_logging_info(self, l): self.app.logger.propagate = True - self.app.logger.info('test info') - l.check( - ('flask.app', 'INFO', 'test info') - ) + self.app.logger.info("test info") + l.check(("flask.app", "INFO", "test info")) log_record = l.records[0] self.assertEqual(log_record.clientip, None) diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index cf799fb78..4d3e195bf 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -18,486 +18,431 @@ class TestSerializer(TestBase): def test_serialize_array_of_tipes(self): # see #1112. schema = { - 'val': { - 'type': 'dict', - 'schema': { - 'x': {'type': ['string', 'number']}, - 'timestamp': {'type': 'datetime'} - } + "val": { + "type": "dict", + "schema": { + "x": {"type": ["string", "number"]}, + "timestamp": {"type": "datetime"}, + }, } } - doc = {'val': {'x': '1', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + doc = {"val": {"x": "1", "timestamp": "Tue, 06 Nov 2012 10:33:31 GMT"}} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertEqual(serialized['val']['x'], 1) - self.assertTrue(isinstance(serialized['val']['timestamp'], datetime)) + self.assertEqual(serialized["val"]["x"], 1) + self.assertTrue(isinstance(serialized["val"]["timestamp"], datetime)) - doc = {'val': {'x': 's', 'timestamp': 'Tue, 06 Nov 2012 10:33:31 GMT'}} + doc = {"val": {"x": "s", "timestamp": "Tue, 06 Nov 2012 10:33:31 GMT"}} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertEqual(serialized['val']['x'], 's') - self.assertTrue(isinstance(serialized['val']['timestamp'], datetime)) + self.assertEqual(serialized["val"]["x"], "s") + self.assertTrue(isinstance(serialized["val"]["timestamp"], datetime)) def test_serialize_subdocument(self): # tests fix for #244, serialization of sub-documents. - schema = {'personal': {'type': 'dict', - 'schema': {'best_friend': {'type': 'objectid'}, - 'born': {'type': 'datetime'}}}, - 'without_type': {}} - doc = {'personal': {'best_friend': '50656e4538345b39dd0414f0', - 'born': 'Tue, 06 Nov 2012 10:33:31 GMT'}, - 'without_type': 'foo'} + schema = { + "personal": { + "type": "dict", + "schema": { + "best_friend": {"type": "objectid"}, + "born": {"type": "datetime"}, + }, + }, + "without_type": {}, + } + doc = { + "personal": { + "best_friend": "50656e4538345b39dd0414f0", + "born": "Tue, 06 Nov 2012 10:33:31 GMT", + }, + "without_type": "foo", + } with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue( - isinstance(serialized['personal']['best_friend'], ObjectId)) - self.assertTrue( - isinstance(serialized['personal']['born'], datetime)) + self.assertTrue(isinstance(serialized["personal"]["best_friend"], ObjectId)) + self.assertTrue(isinstance(serialized["personal"]["born"], datetime)) def test_mongo_serializes(self): schema = { - 'id': {'type': 'objectid'}, - 'date': {'type': 'datetime'}, - 'count': {'type': 'integer'}, - 'average': {'type': 'float'}, - 'dict_valueschema': { - 'valueschema': {'type': 'objectid'} - }, - 'refobj': {'type': 'dbref'}, - 'decobjstring': {'type': 'decimal'}, - 'decobjnumber': {'type': 'decimal'} + "id": {"type": "objectid"}, + "date": {"type": "datetime"}, + "count": {"type": "integer"}, + "average": {"type": "float"}, + "dict_valueschema": {"valueschema": {"type": "objectid"}}, + "refobj": {"type": "dbref"}, + "decobjstring": {"type": "decimal"}, + "decobjnumber": {"type": "decimal"}, } with self.app.app_context(): # Success res = serialize( { - 'id': '50656e4538345b39dd0414f0', - 'date': 'Tue, 06 Nov 2012 10:33:31 GMT', - 'count': 42, - 'average': 42.42, - 'dict_valueschema': { - 'foo1': '50656e4538345b39dd0414f0', - 'foo2': '50656e4538345b39dd0414f0', + "id": "50656e4538345b39dd0414f0", + "date": "Tue, 06 Nov 2012 10:33:31 GMT", + "count": 42, + "average": 42.42, + "dict_valueschema": { + "foo1": "50656e4538345b39dd0414f0", + "foo2": "50656e4538345b39dd0414f0", }, - 'refobj': { - '$id': '50656e4538345b39dd0414f0', - '$col': 'SomeCollection' + "refobj": { + "$id": "50656e4538345b39dd0414f0", + "$col": "SomeCollection", }, - 'decobjstring': "200.0", - 'decobjnumber': 200.0 + "decobjstring": "200.0", + "decobjnumber": 200.0, }, - schema=schema + schema=schema, ) - self.assertTrue(isinstance(res['id'], ObjectId)) - self.assertTrue(isinstance(res['date'], datetime)) - self.assertTrue(isinstance(res['count'], int)) - self.assertTrue(isinstance(res['average'], float)) - - ks = res['dict_valueschema'] - self.assertTrue(isinstance(ks['foo1'], ObjectId)) - self.assertTrue(isinstance(ks['foo2'], ObjectId)) - self.assertTrue(isinstance(res['refobj'], DBRef)) - self.assertTrue(isinstance(res['decobjstring'], - decimal128.Decimal128)) - self.assertTrue(isinstance(res['decobjnumber'], - decimal128.Decimal128)) + self.assertTrue(isinstance(res["id"], ObjectId)) + self.assertTrue(isinstance(res["date"], datetime)) + self.assertTrue(isinstance(res["count"], int)) + self.assertTrue(isinstance(res["average"], float)) + + ks = res["dict_valueschema"] + self.assertTrue(isinstance(ks["foo1"], ObjectId)) + self.assertTrue(isinstance(ks["foo2"], ObjectId)) + self.assertTrue(isinstance(res["refobj"], DBRef)) + self.assertTrue(isinstance(res["decobjstring"], decimal128.Decimal128)) + self.assertTrue(isinstance(res["decobjnumber"], decimal128.Decimal128)) def test_non_blocking_on_simple_field_serialization_exception(self): schema = { - 'extract_time': {'type': 'datetime'}, - 'date': {'type': 'datetime'}, - 'total': {'type': 'integer'} + "extract_time": {"type": "datetime"}, + "date": {"type": "datetime"}, + "total": {"type": "integer"}, } with self.app.app_context(): # Success res = serialize( { - 'extract_time': 'Tue, 06 Nov 2012 10:33:31 GMT', - 'date': 'Tue, 06 Nov 2012 10:33:31 GMT', - 'total': 'r123' + "extract_time": "Tue, 06 Nov 2012 10:33:31 GMT", + "date": "Tue, 06 Nov 2012 10:33:31 GMT", + "total": "r123", }, - schema=schema + schema=schema, ) # this has been left untouched as it could not be serialized. - self.assertEqual(res['total'], 'r123') + self.assertEqual(res["total"], "r123") # these have been both serialized. - self.assertTrue(isinstance(res['extract_time'], datetime)) - self.assertTrue(isinstance(res['date'], datetime)) + self.assertTrue(isinstance(res["extract_time"], datetime)) + self.assertTrue(isinstance(res["date"], datetime)) def test_serialize_lists_of_lists(self): # serialize should handle list of lists of basic types schema = { - 'l_of_l': { - 'type': 'list', - 'schema': { - 'type': 'list', - 'schema': { - 'type': 'objectid' - } - } + "l_of_l": { + "type": "list", + "schema": {"type": "list", "schema": {"type": "objectid"}}, } } doc = { - 'l_of_l': [ - ['50656e4538345b39dd0414f0', '50656e4538345b39dd0414f0'], - ['50656e4538345b39dd0414f0', '50656e4538345b39dd0414f0'] + "l_of_l": [ + ["50656e4538345b39dd0414f0", "50656e4538345b39dd0414f0"], + ["50656e4538345b39dd0414f0", "50656e4538345b39dd0414f0"], ] } with self.app.app_context(): serialized = serialize(doc, schema=schema) - for sublist in serialized['l_of_l']: + for sublist in serialized["l_of_l"]: for item in sublist: self.assertTrue(isinstance(item, ObjectId)) # serialize should handle list of lists of dicts schema = { - 'l_of_l': { - 'type': 'list', - 'schema': { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - '_id': { - 'type': 'objectid' - } - } - } - } + "l_of_l": { + "type": "list", + "schema": { + "type": "list", + "schema": {"type": "dict", "schema": {"_id": {"type": "objectid"}}}, + }, } } doc = { - 'l_of_l': [ + "l_of_l": [ [ - {'_id': '50656e4538345b39dd0414f0'}, - {'_id': '50656e4538345b39dd0414f0'} + {"_id": "50656e4538345b39dd0414f0"}, + {"_id": "50656e4538345b39dd0414f0"}, ], [ - {'_id': '50656e4538345b39dd0414f0'}, - {'_id': '50656e4538345b39dd0414f0'} + {"_id": "50656e4538345b39dd0414f0"}, + {"_id": "50656e4538345b39dd0414f0"}, ], ] } with self.app.app_context(): serialized = serialize(doc, schema=schema) - for sublist in serialized['l_of_l']: + for sublist in serialized["l_of_l"]: for item in sublist: - self.assertTrue(isinstance(item['_id'], ObjectId)) + self.assertTrue(isinstance(item["_id"], ObjectId)) def test_dbref_serialize_lists_of_lists(self): # serialize should handle list of lists of basic types schema = { - 'l_of_l': { - 'type': 'list', - 'schema': { - 'type': 'list', - 'schema': { - 'type': 'dbref' - } - } + "l_of_l": { + "type": "list", + "schema": {"type": "list", "schema": {"type": "dbref"}}, } } doc = { - 'l_of_l': [ - [{'$col': 'SomeCollection', '$id': '50656e4538345b39dd0414f0'}, - {'$col': 'SomeCollection', '$id': '50656e4538345b39dd0414f0'} - ], - [{'$col': 'SomeCollection', '$id': '50656e4538345b39dd0414f0'}, - {'$col': 'SomeCollection', '$id': '50656e4538345b39dd0414f0'} - ] + "l_of_l": [ + [ + {"$col": "SomeCollection", "$id": "50656e4538345b39dd0414f0"}, + {"$col": "SomeCollection", "$id": "50656e4538345b39dd0414f0"}, + ], + [ + {"$col": "SomeCollection", "$id": "50656e4538345b39dd0414f0"}, + {"$col": "SomeCollection", "$id": "50656e4538345b39dd0414f0"}, + ], ] } with self.app.app_context(): serialized = serialize(doc, schema=schema) - for sublist in serialized['l_of_l']: + for sublist in serialized["l_of_l"]: for item in sublist: self.assertTrue(isinstance(item, DBRef)) # serialize should handle list of lists of dicts schema = { - 'l_of_l': { - 'type': 'list', - 'schema': { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - '_id': { - 'type': 'dbref' - } - } - } - } + "l_of_l": { + "type": "list", + "schema": { + "type": "list", + "schema": {"type": "dict", "schema": {"_id": {"type": "dbref"}}}, + }, } } doc = { - 'l_of_l': [ + "l_of_l": [ [ - {'_id': {'$col': 'SomeCollection', - '$id': '50656e4538345b39dd0414f0'} - }, - {'_id': {'$col': 'SomeCollection', - '$id': '50656e4538345b39dd0414f0'} - } + { + "_id": { + "$col": "SomeCollection", + "$id": "50656e4538345b39dd0414f0", + } + }, + { + "_id": { + "$col": "SomeCollection", + "$id": "50656e4538345b39dd0414f0", + } + }, ], [ - {'_id': {'$col': 'SomeCollection', - '$id': '50656e4538345b39dd0414f0'} - }, - {'_id': {'$col': 'SomeCollection', - '$id': '50656e4538345b39dd0414f0'} - } + { + "_id": { + "$col": "SomeCollection", + "$id": "50656e4538345b39dd0414f0", + } + }, + { + "_id": { + "$col": "SomeCollection", + "$id": "50656e4538345b39dd0414f0", + } + }, ], ] } with self.app.app_context(): serialized = serialize(doc, schema=schema) - for sublist in serialized['l_of_l']: + for sublist in serialized["l_of_l"]: for item in sublist: - self.assertTrue(isinstance(item['_id'], DBRef)) + self.assertTrue(isinstance(item["_id"], DBRef)) def test_serialize_null_dictionary(self): # Serialization should continue after encountering a null value dict # field. Field may be nullable, or error will be caught in validation. schema = { - 'nullable_dict': { - 'type': 'dict', - 'nullable': True, - 'schema': { - 'simple_field': { - 'type': 'number' - } - } + "nullable_dict": { + "type": "dict", + "nullable": True, + "schema": {"simple_field": {"type": "number"}}, } } - doc = { - 'nullable_dict': None - } + doc = {"nullable_dict": None} with self.app.app_context(): try: serialize(doc, schema=schema) except Exception: - self.assertTrue(False, "Serializing null dictionaries should " - "not raise an exception.") + self.assertTrue( + False, + "Serializing null dictionaries should " "not raise an exception.", + ) def test_serialize_null_list(self): schema = { - 'nullable_list': { - 'type': 'list', - 'nullable': True, - 'schema': { - 'type': 'objectid' - } + "nullable_list": { + "type": "list", + "nullable": True, + "schema": {"type": "objectid"}, } } - doc = { - 'nullable_list': None - } + doc = {"nullable_list": None} with self.app.app_context(): try: serialize(doc, schema=schema) except Exception: - self.fail('Serializing null lists' - ' should not raise an exception') + self.fail("Serializing null lists" " should not raise an exception") schema = { - 'nullable_list': { - 'type': 'list', - 'nullable': True, - 'schema': { - 'type': 'dbref' - } + "nullable_list": { + "type": "list", + "nullable": True, + "schema": {"type": "dbref"}, } } - doc = { - 'nullable_list': None - } + doc = {"nullable_list": None} with self.app.app_context(): try: serialize(doc, schema=schema) except Exception: - self.fail('Serializing null lists' - ' should not raise an exception') + self.fail("Serializing null lists" " should not raise an exception") def test_serialize_number(self): - schema = { - 'anumber': { - 'type': 'number', - } - } - for expected_type, value in [(int, '35'), (float, '3.5')]: - doc = { - 'anumber': value - } + schema = {"anumber": {"type": "number"}} + for expected_type, value in [(int, "35"), (float, "3.5")]: + doc = {"anumber": value} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue( - isinstance(serialized['anumber'], expected_type) - ) + self.assertTrue(isinstance(serialized["anumber"], expected_type)) def test_serialize_boolean(self): - schema = {'bool': {'type': 'boolean'}} + schema = {"bool": {"type": "boolean"}} with self.app.app_context(): - for val in [1, '1', 0, '0', 'true', 'True', 'false', 'False']: - doc = {'bool': val} + for val in [1, "1", 0, "0", "true", "True", "false", "False"]: + doc = {"bool": val} serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['bool'], bool)) + self.assertTrue(isinstance(serialized["bool"], bool)) def test_serialize_inside_x_of_rules(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - schema = { - 'x_of-field': { - x_of: [ - {'type': 'objectid'}, - {'required': True} - ] - } - } - doc = {'x_of-field': '50656e4538345b39dd0414f0'} + for x_of in ["allof", "anyof", "oneof", "noneof"]: + schema = {"x_of-field": {x_of: [{"type": "objectid"}, {"required": True}]}} + doc = {"x_of-field": "50656e4538345b39dd0414f0"} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + self.assertTrue(isinstance(serialized["x_of-field"], ObjectId)) def test_serialize_alongside_x_of_rules(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: - schema = OrderedDict([ - ('x_of-field', { - x_of: [ - {'type': 'objectid'}, - {'required': True} - ] - }), - ('oid-field', {'type': 'objectid'}) - ]) - doc = OrderedDict([('x_of-field', '50656e4538345b39dd0414f0'), - ('oid-field', '50656e4538345b39dd0414f0')]) + for x_of in ["allof", "anyof", "oneof", "noneof"]: + schema = OrderedDict( + [ + ("x_of-field", {x_of: [{"type": "objectid"}, {"required": True}]}), + ("oid-field", {"type": "objectid"}), + ] + ) + doc = OrderedDict( + [ + ("x_of-field", "50656e4538345b39dd0414f0"), + ("oid-field", "50656e4538345b39dd0414f0"), + ] + ) with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) - self.assertTrue(isinstance(serialized['oid-field'], ObjectId)) + self.assertTrue(isinstance(serialized["x_of-field"], ObjectId)) + self.assertTrue(isinstance(serialized["oid-field"], ObjectId)) def test_serialize_list_alongside_x_of_rules(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: schema = { - 'x_of-field': { + "x_of-field": { "type": "list", x_of: [ - {"schema": {'type': 'objectid'}}, - {"schema": {'type': 'datetime'}} - ] + {"schema": {"type": "objectid"}}, + {"schema": {"type": "datetime"}}, + ], } } - doc = {'x_of-field': ['50656e4538345b39dd0414f0']} + doc = {"x_of-field": ["50656e4538345b39dd0414f0"]} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['x_of-field'][0], - ObjectId)) + self.assertTrue(isinstance(serialized["x_of-field"][0], ObjectId)) def test_serialize_inside_nested_x_of_rules(self): schema = { - 'nested-x_of-field': { - 'oneof': [ + "nested-x_of-field": { + "oneof": [ { - 'anyof': [ - {'type': 'objectid'}, - {'type': 'datetime'} - ], - 'required': True + "anyof": [{"type": "objectid"}, {"type": "datetime"}], + "required": True, }, - { - 'allof': [ - {'type': 'boolean'}, - {'required': True} - ] - } + {"allof": [{"type": "boolean"}, {"required": True}]}, ] } } - doc = {'nested-x_of-field': '50656e4538345b39dd0414f0'} + doc = {"nested-x_of-field": "50656e4538345b39dd0414f0"} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue( - isinstance(serialized['nested-x_of-field'], ObjectId)) + self.assertTrue(isinstance(serialized["nested-x_of-field"], ObjectId)) def test_serialize_inside_x_of_typesavers(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: schema = { - 'x_of-field': { - '{0}_type'.format(x_of): ['objectid', 'float', 'boolean'] + "x_of-field": { + "{0}_type".format(x_of): ["objectid", "float", "boolean"] } } - doc = {'x_of-field': '50656e4538345b39dd0414f0'} + doc = {"x_of-field": "50656e4538345b39dd0414f0"} with self.app.app_context(): serialized = serialize(doc, schema=schema) - self.assertTrue(isinstance(serialized['x_of-field'], ObjectId)) + self.assertTrue(isinstance(serialized["x_of-field"], ObjectId)) def test_serialize_inside_list_of_x_of_rules(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: schema = { - 'list-field': { - 'type': 'list', - 'schema': { - x_of: [ - { - 'type': 'objectid', - 'required': True} - ] - } + "list-field": { + "type": "list", + "schema": {x_of: [{"type": "objectid", "required": True}]}, } } - doc = {'list-field': ['50656e4538345b39dd0414f0']} + doc = {"list-field": ["50656e4538345b39dd0414f0"]} with self.app.app_context(): serialized = serialize(doc, schema=schema) - serialized_oid = serialized['list-field'][0] + serialized_oid = serialized["list-field"][0] self.assertTrue(isinstance(serialized_oid, ObjectId)) def test_serialize_inside_list_of_schema_of_x_of_rules(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: schema = { - 'list-field': { - 'type': 'list', - 'schema': { + "list-field": { + "type": "list", + "schema": { x_of: [ { - 'type': 'dict', - 'schema': { - 'x_of-field': { - 'type': 'objectid', - 'required': True - } - } + "type": "dict", + "schema": { + "x_of-field": {"type": "objectid", "required": True} + }, } ] - } + }, } } - doc = {'list-field': [{'x_of-field': '50656e4538345b39dd0414f0'}]} + doc = {"list-field": [{"x_of-field": "50656e4538345b39dd0414f0"}]} with self.app.app_context(): serialized = serialize(doc, schema=schema) - serialized_oid = serialized['list-field'][0]['x_of-field'] + serialized_oid = serialized["list-field"][0]["x_of-field"] self.assertTrue(isinstance(serialized_oid, ObjectId)) def test_serialize_inside_list_of_x_of_typesavers(self): - for x_of in ['allof', 'anyof', 'oneof', 'noneof']: + for x_of in ["allof", "anyof", "oneof", "noneof"]: schema = { - 'list-field': { - 'type': 'list', - 'schema': { - '{0}_type'.format(x_of): [ - 'objectid', 'float', 'boolean' - ] - } + "list-field": { + "type": "list", + "schema": { + "{0}_type".format(x_of): ["objectid", "float", "boolean"] + }, } } - doc = {'list-field': ['50656e4538345b39dd0414f0']} + doc = {"list-field": ["50656e4538345b39dd0414f0"]} with self.app.app_context(): serialized = serialize(doc, schema=schema) - serialized_oid = serialized['list-field'][0] + serialized_oid = serialized["list-field"][0] self.assertTrue(isinstance(serialized_oid, ObjectId)) @@ -512,47 +457,14 @@ def compare_recursive(a, b): return True document = { - 'a.b': 1, - 'c.d': { - 'e.f': { - 'g': 1, - 'h': 2, - }, - 'e.f.i': {'j.k': 3, - }, - }, - 'l': [ - { - 'm.n': 4, - }, - ], + "a.b": 1, + "c.d": {"e.f": {"g": 1, "h": 2}, "e.f.i": {"j.k": 3}}, + "l": [{"m.n": 4}], } expected_result = { - 'a': { - 'b': 1, - }, - 'c': { - 'd': { - 'e': { - 'f': { - 'g': 1, - 'h': 2, - 'i': { - 'j': { - 'k': 3, - }, - }, - }, - }, - }, - }, - 'l': [ - { - 'm': { - 'n': 4, - }, - }, - ], + "a": {"b": 1}, + "c": {"d": {"e": {"f": {"g": 1, "h": 2, "i": {"j": {"k": 3}}}}}}, + "l": [{"m": {"n": 4}}], } normalize_dotted_fields(document) self.assertTrue(compare_recursive(document, expected_result)) @@ -561,56 +473,59 @@ def compare_recursive(a, b): class TestOpLogBase(TestBase): def setUp(self): super(TestOpLogBase, self).setUp() - self.test_field, self.test_value = 'ref', "1234567890123456789054321" + self.test_field, self.test_value = "ref", "1234567890123456789054321" self.data = {self.test_field: self.test_value} self.test_client = self.app.test_client() - self.headers = [(('Content-Type', 'application/json'))] + self.headers = [(("Content-Type", "application/json"))] def oplog_reset(self): self.app._init_oplog() - self.app.register_resource('oplog', self.domain['oplog']) + self.app.register_resource("oplog", self.domain["oplog"]) - settings = self.app.config['DOMAIN']['oplog'] - datasource = settings['datasource'] - schema = settings['schema'] - datasource['projection'] = {} + settings = self.app.config["DOMAIN"]["oplog"] + datasource = settings["datasource"] + schema = settings["schema"] + datasource["projection"] = {} self.app._set_resource_projection(datasource, schema, settings) - def oplog_get(self, url='/oplog'): + def oplog_get(self, url="/oplog"): r = self.test_client.get(url) return self.parse_response(r) def assertOpLogEntry(self, entry, op, user=None): - self.assertTrue('r' in entry) - self.assertTrue('i' in entry) + self.assertTrue("r" in entry) + self.assertTrue("i" in entry) self.assertTrue(config.LAST_UPDATED in entry) self.assertTrue(config.DATE_CREATED in entry) - self.assertTrue('o' in entry) - self.assertEqual(entry['o'], op) - self.assertTrue('127.0.0.1' in entry['ip']) - if op in self.app.config['OPLOG_CHANGE_METHODS']: - self.assertTrue('c' in entry) - self.assertTrue('u' in entry) + self.assertTrue("o" in entry) + self.assertEqual(entry["o"], op) + self.assertTrue("127.0.0.1" in entry["ip"]) + if op in self.app.config["OPLOG_CHANGE_METHODS"]: + self.assertTrue("c" in entry) + self.assertTrue("u" in entry) if user: - self.assertTrue(user in entry['u']) + self.assertTrue(user in entry["u"]) else: - self.assertTrue('n/a' in entry['u']) + self.assertTrue("n/a" in entry["u"]) class TestOpLogEndpointDisabled(TestOpLogBase): def setUp(self): super(TestOpLogEndpointDisabled, self).setUp() - self.app.config['OPLOG'] = True + self.app.config["OPLOG"] = True from eve.default_settings import OPLOG_CHANGE_METHODS - self.app.config['OPLOG_CHANGE_METHODS'] = OPLOG_CHANGE_METHODS + + self.app.config["OPLOG_CHANGE_METHODS"] = OPLOG_CHANGE_METHODS self.oplog_reset() def test_post_oplog(self): - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) # oplog endpoint is not available. r, status = self.oplog_get() @@ -620,187 +535,205 @@ def test_post_oplog(self): db = self.connection[MONGO_DBNAME] cursor = db.oplog.find() self.assertEqual(cursor.count(), 1) - self.assertOpLogEntry(cursor[0], 'POST') + self.assertOpLogEntry(cursor[0], "POST") class TestOpLogEndpointEnabled(TestOpLogBase): def setUp(self): super(TestOpLogEndpointEnabled, self).setUp() - self.app.config['OPLOG'] = True - self.app.config['OPLOG_ENDPOINT'] = 'oplog' + self.app.config["OPLOG"] = True + self.app.config["OPLOG_ENDPOINT"] = "oplog" self.oplog_reset() def test_oplog_hook(self): def oplog_callback(resource, entries): for entry in entries: - entry['extra'] = {'customfield': 'customvalue'} + entry["extra"] = {"customfield": "customvalue"} self.app.on_oplog_push += oplog_callback - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) # oplog enpoint does not expose the 'extra' field r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST') - self.assertTrue('extra' not in oplog_entry) + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST") + self.assertTrue("extra" not in oplog_entry) # however the oplog collection has the field. db = self.connection[MONGO_DBNAME] cursor = db.oplog.find() self.assertEqual(cursor.count(), 1) oplog_entry = cursor[0] - self.assertTrue('extra' in oplog_entry) - self.assertTrue('customvalue' in oplog_entry['extra']['customfield']) + self.assertTrue("extra" in oplog_entry) + self.assertTrue("customvalue" in oplog_entry["extra"]["customfield"]) # enable 'extra' field for the endpoint - self.app.config['OPLOG_RETURN_EXTRA_FIELD'] = True + self.app.config["OPLOG_RETURN_EXTRA_FIELD"] = True self.oplog_reset() # now the oplog endpoint includes the 'extra' field r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST') - self.assertTrue('extra' in oplog_entry) - self.assertTrue('customvalue' in oplog_entry['extra']['customfield']) + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST") + self.assertTrue("extra" in oplog_entry) + self.assertTrue("customvalue" in oplog_entry["extra"]["customfield"]) def test_post_oplog(self): r = self.test_client.post( self.different_resource_url, - data=json.dumps({'username': 'test', 'ref': - '1234567890123456789012345'}), - headers=self.headers, environ_base={'REMOTE_ADDR': '127.0.0.1'}) + data=json.dumps({"username": "test", "ref": "1234567890123456789012345"}), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST') - self.assertTrue('extra' not in oplog_entry) + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST") + self.assertTrue("extra" not in oplog_entry) def test_patch_oplog(self): - self.headers.append(('If-Match', self.item_etag)) - r = self.test_client.patch(self.item_id_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.headers.append(("If-Match", self.item_etag)) + r = self.test_client.patch( + self.item_id_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'PATCH') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "PATCH") def test_put_oplog(self): - self.headers.append(('If-Match', self.item_etag)) - r = self.test_client.put(self.item_id_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.headers.append(("If-Match", self.item_etag)) + r = self.test_client.put( + self.item_id_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'PUT') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "PUT") def test_put_oplog_does_not_alter_document(self): """ Make sure we don't alter document ETag when performing an oplog_push. See #590. """ - self.headers.append(('If-Match', self.item_etag)) - r = self.test_client.put(self.item_id_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) - - etag1 = json.loads(r.get_data())['_etag'] - etag2 = json.loads( - self.test_client.get(self.item_id_url).get_data())['_etag'] + self.headers.append(("If-Match", self.item_etag)) + r = self.test_client.put( + self.item_id_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) + + etag1 = json.loads(r.get_data())["_etag"] + etag2 = json.loads(self.test_client.get(self.item_id_url).get_data())["_etag"] self.assertEqual(etag1, etag2) def test_delete_oplog(self): - self.headers.append(('If-Match', self.item_etag)) - r = self.test_client.delete(self.item_id_url, - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.headers.append(("If-Match", self.item_etag)) + r = self.test_client.delete( + self.item_id_url, + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'DELETE') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "DELETE") def test_soft_delete_oplog(self): r, s = self.parse_response(self.test_client.get(self.item_id_url)) doc_date = r[config.LAST_UPDATED] time.sleep(1) - self.domain[self.known_resource]['soft_delete'] = True + self.domain[self.known_resource]["soft_delete"] = True - self.headers.append(('If-Match', self.item_etag)) - r = self.test_client.delete(self.item_id_url, - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.headers.append(("If-Match", self.item_etag)) + r = self.test_client.delete( + self.item_id_url, + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'DELETE') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "DELETE") self.assertTrue(doc_date != oplog_entry[config.LAST_UPDATED]) def test_post_oplog_with_basic_auth(self): - self.domain['contacts']['authentication'] = ValidBasicAuth - self.headers.append(('Authorization', 'Basic YWRtaW46c2VjcmV0')) - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.domain["contacts"]["authentication"] = ValidBasicAuth + self.headers.append(("Authorization", "Basic YWRtaW46c2VjcmV0")) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST', 'admin') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST", "admin") def test_post_oplog_with_token_auth(self): - self.domain['contacts']['authentication'] = ValidTokenAuth - self.headers.append(('Authorization', 'Basic dGVzdF90b2tlbjo=')) - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.domain["contacts"]["authentication"] = ValidTokenAuth + self.headers.append(("Authorization", "Basic dGVzdF90b2tlbjo=")) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST', 'test_token') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST", "test_token") def test_post_oplog_with_hmac_auth(self): - self.domain['contacts']['authentication'] = ValidHMACAuth - self.headers.append(('Authorization', 'admin:secret')) - r = self.test_client.post(self.known_resource_url, - data=json.dumps(self.data), - headers=self.headers, - environ_base={'REMOTE_ADDR': '127.0.0.1'}) + self.domain["contacts"]["authentication"] = ValidHMACAuth + self.headers.append(("Authorization", "admin:secret")) + r = self.test_client.post( + self.known_resource_url, + data=json.dumps(self.data), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) r, status = self.oplog_get() self.assert200(status) - self.assertEqual(len(r['_items']), 1) - oplog_entry = r['_items'][0] - self.assertOpLogEntry(oplog_entry, 'POST', 'admin') + self.assertEqual(len(r["_items"]), 1) + oplog_entry = r["_items"][0] + self.assertOpLogEntry(oplog_entry, "POST", "admin") - def patch(self, url, data, headers=[], content_type='application/json'): - headers.append(('Content-Type', content_type)) - headers.append(('If-Match', self.item_etag)) + def patch(self, url, data, headers=[], content_type="application/json"): + headers.append(("Content-Type", content_type)) + headers.append(("If-Match", self.item_etag)) r = self.test_client.patch(url, data=json.dumps(data), headers=headers) return self.parse_response(r) - def put(self, url, data, headers=[], content_type='application/json'): - headers.append(('Content-Type', content_type)) - headers.append(('If-Match', self.item_etag)) + def put(self, url, data, headers=[], content_type="application/json"): + headers.append(("Content-Type", content_type)) + headers.append(("If-Match", self.item_etag)) r = self.test_client.put(url, data=json.dumps(data), headers=headers) return self.parse_response(r) @@ -808,5 +741,5 @@ def put(self, url, data, headers=[], content_type='application/json'): class TestTickets(TestBase): def test_ticket_681(self): # See https://github.com/pyeve/eve/issues/681 - with self.app.test_request_context('not_an_existing_endpoint'): - self.app.data.driver.db['again'] + with self.app.test_request_context("not_an_existing_endpoint"): + self.app.data.driver.db["again"] diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index d7179ab2f..d59b89712 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -14,10 +14,10 @@ class TestDelete(TestBase): def setUp(self): super(TestDelete, self).setUp() # Etag used to delete an item (a contact) - self.etag_headers = [('If-Match', self.item_etag)] + self.etag_headers = [("If-Match", self.item_etag)] def test_unknown_resource(self): - url = '%s%s/' % (self.unknown_resource_url, self.item_id) + url = "%s%s/" % (self.unknown_resource_url, self.item_id) _, status = self.delete(url) self.assert404(status) @@ -25,17 +25,20 @@ def test_bulk_delete_id_field(self): etag_check = self.app.config["IF_MATCH"] self.app.config["IF_MATCH"] = False products, _ = self.get(self.products) - list_products_skus = [product["parent_product"] for product in - products["_items"] if "parent_product" in - product] + list_products_skus = [ + product["parent_product"] + for product in products["_items"] + if "parent_product" in product + ] # Deletion of all the product in the first cart url = self.child_products_url.replace( - '', list_products_skus[0]) + '', list_products_skus[0] + ) _, status = self.delete(url) self.assert204(status) _, status = self.get(url) self.assert404(status) - products_url = '%s/%s' % (self.products, list_products_skus[0]) + products_url = "%s/%s" % (self.products, list_products_skus[0]) _, status = self.delete(products_url) self.assert204(status) _, status = self.get(products_url) @@ -47,34 +50,33 @@ def test_bulk_delete_id_field(self): def test_delete_from_resource_endpoint(self): r, status = self.delete(self.known_resource_url) self.assert204(status) - r, status = self.parse_response(self.test_client.get( - self.known_resource_url)) + r, status = self.parse_response(self.test_client.get(self.known_resource_url)) self.assert200(status) - self.assertEqual(len(r['_items']), 0) + self.assertEqual(len(r["_items"]), 0) def test_delete_from_resource_endpoint_write_concern(self): # should get a 500 since there's no replicaset on the mongod instance - self.domain['contacts']['mongo_write_concern'] = {'w': 2} + self.domain["contacts"]["mongo_write_concern"] = {"w": 2} _, status = self.delete(self.known_resource_url) self.assert500(status) def test_delete_from_resource_endpoint_different_resource(self): r, status = self.delete(self.different_resource_url) self.assert204(status) - r, status = self.parse_response(self.test_client.get( - self.different_resource_url)) + r, status = self.parse_response( + self.test_client.get(self.different_resource_url) + ) self.assert200(status) - self.assertEqual(len(r['_items']), 0) + self.assertEqual(len(r["_items"]), 0) # deletion of 'users' will still lave 'contacts' untouched (same db # collection) - r, status = self.parse_response(self.test_client.get( - self.known_resource_url)) + r, status = self.parse_response(self.test_client.get(self.known_resource_url)) self.assert200(status) - self.assertEqual(len(r['_items']), 25) + self.assertEqual(len(r["_items"]), 25) def test_delete_empty_resource(self): - url = '%s%s/' % (self.empty_resource_url, self.item_id) + url = "%s%s/" % (self.empty_resource_url, self.item_id) _, status = self.delete(url) self.assert404(status) @@ -83,12 +85,12 @@ def test_delete_readonly_resource(self): self.assert405(status) def test_delete_readonly_resource_with_override(self): - headers = [('X-HTTP-Method-Override', 'DELETE')] + headers = [("X-HTTP-Method-Override", "DELETE")] r = self.test_client.get(self.readonly_resource_url, headers=headers) self.assert405(r.status_code) def test_delete_unknown_item(self): - url = '%s%s/' % (self.known_resource_url, self.unknown_item_id) + url = "%s%s/" % (self.known_resource_url, self.unknown_item_id) _, status = self.delete(url) self.assert404(status) @@ -97,7 +99,7 @@ def test_delete_ifmatch_missing(self): self.assert428(status) def test_ifmatch_missing_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False + self.app.config["ENFORCE_IF_MATCH"] = False r, status = self.delete(self.item_id_url) self.assert204(status) @@ -105,13 +107,13 @@ def test_ifmatch_missing_enforce_ifmatch_disabled(self): self.assert404(r.status_code) def test_delete_ifmatch_disabled(self): - self.app.config['IF_MATCH'] = False + self.app.config["IF_MATCH"] = False _, status = self.delete(self.item_id_url) self.assert204(status) def test_ifmatch_disabled_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - self.app.config['IF_MATCH'] = False + self.app.config["ENFORCE_IF_MATCH"] = False + self.app.config["IF_MATCH"] = False r, status = self.delete(self.item_id_url) self.assert204(status) @@ -119,15 +121,15 @@ def test_ifmatch_disabled_enforce_ifmatch_disabled(self): self.assert404(r.status_code) def test_delete_ifmatch_bad_etag(self): - _, status = self.delete(self.item_id_url, - headers=[('If-Match', 'not-quite-right')]) + _, status = self.delete( + self.item_id_url, headers=[("If-Match", "not-quite-right")] + ) self.assert412(status) def test_ifmatch_bad_etag_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False + self.app.config["ENFORCE_IF_MATCH"] = False _, status = self.delete( - self.item_id_url, - headers=[('If-Match', 'not-quite-right')] + self.item_id_url, headers=[("If-Match", "not-quite-right")] ) self.assert412(status) @@ -145,14 +147,16 @@ def test_delete_non_existant(self): def test_delete_write_concern(self): # should get a 500 since there's no replicaset on the mongod instance - self.domain['contacts']['mongo_write_concern'] = {'w': 2} - _, status = self.delete(self.item_id_url, - headers=[('If-Match', self.item_etag)]) + self.domain["contacts"]["mongo_write_concern"] = {"w": 2} + _, status = self.delete( + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert500(status) def test_delete_different_resource(self): - r, status = self.delete(self.user_id_url, - headers=[('If-Match', self.user_etag)]) + r, status = self.delete( + self.user_id_url, headers=[("If-Match", self.user_etag)] + ) self.assert204(status) r = self.test_client.get(self.user_id_url) @@ -160,8 +164,7 @@ def test_delete_different_resource(self): def test_delete_with_post_override(self): # POST request with DELETE override turns into a DELETE - headers = [('X-HTTP-Method-Override', 'DELETE'), - ('If-Match', self.item_etag)] + headers = [("X-HTTP-Method-Override", "DELETE"), ("If-Match", self.item_etag)] r = self.test_client.post(self.item_id_url, data={}, headers=headers) self.assert204(r.status_code) @@ -176,32 +179,33 @@ def test_delete_subresource(self): # didn't delete all the users in the datanase. We add one extra invoice # to make sure that the actual count will never be 1 (which would # invalidate the test) - _db.invoices.insert_one({'inv_number': 1}) - response, status = self.get('invoices') - invoices = len(response[self.app.config['ITEMS']]) + _db.invoices.insert_one({"inv_number": 1}) + response, status = self.get("invoices") + invoices = len(response[self.app.config["ITEMS"]]) # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # verify that the only document retrieved is referencing the correct # parent document - response, status = self.get('users/%s/invoices' % fake_contact_id) - person_id = ObjectId(response[self.app.config['ITEMS']][0]['person']) + response, status = self.get("users/%s/invoices" % fake_contact_id) + person_id = ObjectId(response[self.app.config["ITEMS"]][0]["person"]) self.assertEqual(person_id, fake_contact_id) # delete all documents at the sub-resource endpoint - response, status = self.delete('users/%s/invoices' % fake_contact_id) + response, status = self.delete("users/%s/invoices" % fake_contact_id) self.assert204(status) # verify that the no documents are left at the sub-resource endpoint - response, status = self.get('users/%s/invoices' % fake_contact_id) - self.assertEqual(len(response['_items']), 0) + response, status = self.get("users/%s/invoices" % fake_contact_id) + self.assertEqual(len(response["_items"]), 0) # verify that other documents in the invoices collection have not neen # deleted - response, status = self.get('invoices') - self.assertEqual(len(response['_items']), invoices - 1) + response, status = self.get("invoices") + self.assertEqual(len(response["_items"]), invoices - 1) def test_delete_subresource_item(self): _db = self.connection[MONGO_DBNAME] @@ -211,34 +215,35 @@ def test_delete_subresource_item(self): fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id)) + response, status = self.get( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id) + ) etag = response[ETAG] - headers = [('If-Match', etag)] - response, status = self.delete('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id), - headers=headers) + headers = [("If-Match", etag)] + response, status = self.delete( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id), headers=headers + ) self.assert204(status) def test_delete_custom_idfield(self): - response, status = self.get('products?max_results=1') - product = response['_items'][0] - headers = [('If-Match', product[ETAG])] - response, status = self.delete('products/%s' % product['sku'], - headers=headers) + response, status = self.get("products?max_results=1") + product = response["_items"][0] + headers = [("If-Match", product[ETAG])] + response, status = self.delete("products/%s" % product["sku"], headers=headers) self.assert204(status) def test_deleteitem_internal(self): # test that deleteitem_internal is available and working properly. with self.app.test_request_context(self.item_id_url): r, _, _, status = deleteitem_internal( - self.known_resource, concurrency_check=False, - **{'_id': self.item_id}) + self.known_resource, concurrency_check=False, **{"_id": self.item_id} + ) self.assert204(status) r = self.test_client.get(self.item_id_url) @@ -254,15 +259,15 @@ def setUp(self): super(TestSoftDelete, self).setUp() # Enable soft delete - self.app.config['SOFT_DELETE'] = True + self.app.config["SOFT_DELETE"] = True domain = copy.copy(self.domain) for resource, settings in domain.items(): # rebuild resource settings for soft delete - del settings['soft_delete'] + del settings["soft_delete"] self.app.register_resource(resource, settings) # alias for the configured DELETED field name - self.deleted_field = self.app.config['DELETED'] + self.deleted_field = self.app.config["DELETED"] # TestDelete overrides @@ -280,9 +285,9 @@ def test_delete(self): self.assert404(status) self.assertEqual(data.get(self.deleted_field), True) - self.assertNotEqual(data.get('_etag'), self.item_etag) + self.assertNotEqual(data.get("_etag"), self.item_etag) # 404 should still include a status and an error field - self.assertTrue(self.app.config['ERROR'] in data) + self.assertTrue(self.app.config["ERROR"] in data) def test_deleteitem_internal(self): """Deleteitem internal should honor soft delete settings. @@ -290,8 +295,8 @@ def test_deleteitem_internal(self): # test that deleteitem_internal is available and working properly. with self.app.test_request_context(self.item_id_url): r, _, _, status = deleteitem_internal( - self.known_resource, concurrency_check=False, - **{'_id': self.item_id}) + self.known_resource, concurrency_check=False, **{"_id": self.item_id} + ) self.assert204(status) r = self.test_client.get(self.item_id_url) @@ -300,8 +305,9 @@ def test_deleteitem_internal(self): self.assertEqual(data.get(self.deleted_field), True) def test_delete_different_resource(self): - r, status = self.delete(self.user_id_url, - headers=[('If-Match', self.user_etag)]) + r, status = self.delete( + self.user_id_url, headers=[("If-Match", self.user_etag)] + ) self.assert204(status) r = self.test_client.get(self.user_id_url) @@ -329,35 +335,35 @@ def test_restore_softdeleted(self): """Sending a PUT or PATCH to a soft deleted document should restore the document. """ + def soft_delete_item(etag): - r, status = self.delete( - self.item_id_url, headers=[('If-Match', etag)]) + r, status = self.delete(self.item_id_url, headers=[("If-Match", etag)]) self.assert204(status) # GET soft deleted etag return self.test_client.get(self.item_id_url) # Restore via PATCH - deleted_etag = soft_delete_item(self.item_etag).headers['ETag'] + deleted_etag = soft_delete_item(self.item_etag).headers["ETag"] r = self.test_client.patch( self.item_id_url, data=json.dumps({}), - headers=[('Content-Type', 'application/json'), - ('If-Match', deleted_etag)]) + headers=[("Content-Type", "application/json"), ("If-Match", deleted_etag)], + ) self.assert200(r.status_code) r = self.test_client.get(self.item_id_url) self.assert200(r.status_code) - new_etag = r.headers['ETag'] + new_etag = r.headers["ETag"] # Restore via PUT r = soft_delete_item(new_etag) - deleted_etag = r.headers['ETag'] + deleted_etag = r.headers["ETag"] restored_doc = {"ref": "1234567890123456789012345"} r = self.test_client.put( self.item_id_url, data=json.dumps(restored_doc), - headers=[('Content-Type', 'application/json'), - ('If-Match', deleted_etag)]) + headers=[("Content-Type", "application/json"), ("If-Match", deleted_etag)], + ) self.assert200(r.status_code) r = self.test_client.get(self.item_id_url) @@ -371,11 +377,10 @@ def test_multiple_softdelete(self): self.assert204(status) # GET soft deleted etag r = self.test_client.get(self.item_id_url) - new_etag = r.headers['ETag'] + new_etag = r.headers["ETag"] # Second soft DELETE should return 404 Not Found - r, status = self.delete( - self.item_id_url, headers=[('If-Match', new_etag)]) + r, status = self.delete(self.item_id_url, headers=[("If-Match", new_etag)]) self.assert404(status) def test_softdelete_deleted_field(self): @@ -396,27 +401,25 @@ def test_softdelete_show_deleted(self): self.assert204(status) data, status = self.get(self.known_resource) - after_softdelete_count = data[self.app.config['META']]['total'] + after_softdelete_count = data[self.app.config["META"]]["total"] self.assertEqual(after_softdelete_count, self.known_resource_count - 1) data, status = self.get(self.known_resource, query="?show_deleted") - show_deleted_count = data[self.app.config['META']]['total'] + show_deleted_count = data[self.app.config["META"]]["total"] self.assertEqual(show_deleted_count, self.known_resource_count) # Test show_deleted with additional queries - role_query = '?where={"role": "' + self.item['role'] + '"}' + role_query = '?where={"role": "' + self.item["role"] + '"}' data, status = self.get(self.known_resource, query=role_query) - role_count = data[self.app.config['META']]['total'] + role_count = data[self.app.config["META"]]["total"] - data, status = self.get( - self.known_resource, query=role_query + "&show_deleted") - show_deleted_role_count = data[self.app.config['META']]['total'] + data, status = self.get(self.known_resource, query=role_query + "&show_deleted") + show_deleted_role_count = data[self.app.config["META"]]["total"] self.assertEqual(show_deleted_role_count, role_count + 1) # Test explicit _deleted query - data, status = self.get( - self.known_resource, query='?where={"_deleted": true}') - deleted_query_count = data[self.app.config['META']]['total'] + data, status = self.get(self.known_resource, query='?where={"_deleted": true}') + deleted_query_count = data[self.app.config["META"]]["total"] self.assertEqual(deleted_query_count, 1) def test_softdeleted_embedded_doc(self): @@ -429,35 +432,35 @@ def test_softdeleted_embedded_doc(self): fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id fake_contact_url = self.known_resource_url + "/" + str(fake_contact_id) - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) - invoices = self.domain['invoices'] - invoices['embedding'] = True - invoices['schema']['person']['data_relation']['embeddable'] = True + invoices = self.domain["invoices"] + invoices["embedding"] = True + invoices["schema"]["person"]["data_relation"]["embeddable"] = True embedded = '{"person": 1}' - r = self.test_client.get( - self.invoice_id_url + '?embedded=%s' % embedded) + r = self.test_client.get(self.invoice_id_url + "?embedded=%s" % embedded) data, status = self.parse_response(r) self.assert200(status) - self.assertTrue('location' in data['person']) + self.assertTrue("location" in data["person"]) # Get embedded doc etag so we can delete it r = self.test_client.get(fake_contact_url) - embedded_contact_etag = r.headers['ETag'] + embedded_contact_etag = r.headers["ETag"] # Delete embedded contact data, status = self.delete( - fake_contact_url, headers=[('If-Match', embedded_contact_etag)]) + fake_contact_url, headers=[("If-Match", embedded_contact_etag)] + ) self.assert204(status) # embedded 'person' should now be empty - r = self.test_client.get( - self.invoice_id_url + '?embedded=%s' % embedded) + r = self.test_client.get(self.invoice_id_url + "?embedded=%s" % embedded) data, status = self.parse_response(r) self.assert200(status) - self.assertEqual(data['person'], None) + self.assertEqual(data["person"], None) def test_softdeleted_get_response_skips_embedded_expansion(self): """Soft deleted documents should not expand their embedded documents when @@ -469,32 +472,32 @@ def test_softdeleted_get_response_skips_embedded_expansion(self): _db = self.connection[MONGO_DBNAME] fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) - invoices = self.domain['invoices'] - invoices['embedding'] = True - invoices['schema']['person']['data_relation']['embeddable'] = True + invoices = self.domain["invoices"] + invoices["embedding"] = True + invoices["schema"]["person"]["data_relation"]["embeddable"] = True embedded = '{"person": 1}' - r = self.test_client.get( - self.invoice_id_url + '?embedded=%s' % embedded) - invoice_etag = r.headers['ETag'] + r = self.test_client.get(self.invoice_id_url + "?embedded=%s" % embedded) + invoice_etag = r.headers["ETag"] data, status = self.parse_response(r) self.assert200(status) - self.assertTrue('location' in data['person']) + self.assertTrue("location" in data["person"]) # Soft delete document data, status = self.delete( - self.invoice_id_url, headers=[('If-Match', invoice_etag)]) + self.invoice_id_url, headers=[("If-Match", invoice_etag)] + ) self.assert204(status) # Document in 404 should not expand person - r = self.test_client.get( - self.invoice_id_url + '?embedded=%s' % embedded) + r = self.test_client.get(self.invoice_id_url + "?embedded=%s" % embedded) data, status = self.parse_response(r) self.assert404(status) - self.assertEqual(data['person'], str(fake_contact_id)) + self.assertEqual(data["person"], str(fake_contact_id)) def test_softdelete_caching(self): """404 Not Found responses after soft delete should be cacheable @@ -505,14 +508,16 @@ def test_softdelete_caching(self): # delete should have invalidated any previously cached 200 responses r = self.test_client.get( - self.item_id_url, headers=[('If-None-Match', self.item_etag)]) + self.item_id_url, headers=[("If-None-Match", self.item_etag)] + ) self.assert404(r.status_code) - post_delete_etag = r.headers['ETag'] + post_delete_etag = r.headers["ETag"] # validate cached 404 response data r = status = self.test_client.get( - self.item_id_url, headers=[('If-None-Match', post_delete_etag)]) + self.item_id_url, headers=[("If-None-Match", post_delete_etag)] + ) self.assert304(r.status_code) def test_softdelete_datalayer(self): @@ -528,25 +533,25 @@ def test_softdelete_datalayer(self): # find_one should only return item if a request w/ show_deleted == # True is passed or if the deleted field is part of the lookup req = ParsedRequest() - doc = self.app.data.find_one( - self.known_resource, req, _id=self.item_id) + doc = self.app.data.find_one(self.known_resource, req, _id=self.item_id) self.assertEqual(doc, None) req.show_deleted = True - doc = self.app.data.find_one( - self.known_resource, req, _id=self.item_id) + doc = self.app.data.find_one(self.known_resource, req, _id=self.item_id) self.assertNotEqual(doc, None) self.assertEqual(doc.get(self.deleted_field), True) req.show_deleted = False doc = self.app.data.find_one( - self.known_resource, req, _id=self.item_id, _deleted=True) + self.known_resource, req, _id=self.item_id, _deleted=True + ) self.assertNotEqual(doc, None) self.assertEqual(doc.get(self.deleted_field), True) # find_one_raw should always return a document, soft deleted or not doc = self.app.data.find_one_raw( - self.known_resource, _id=ObjectId(self.item_id)) + self.known_resource, _id=ObjectId(self.item_id) + ) self.assertNotEqual(doc, None) self.assertEqual(doc.get(self.deleted_field), True) @@ -564,57 +569,62 @@ def test_softdelete_datalayer(self): req.show_deleted = False docs = self.app.data.find( - self.known_resource, req, {self.deleted_field: True}) + self.known_resource, req, {self.deleted_field: True} + ) deleted_count = docs.count() self.assertEqual(deleted_count, 1) # find_list_of_ids will return deleted documents if given their id docs = self.app.data.find_list_of_ids( - self.known_resource, [ObjectId(self.item_id)]) + self.known_resource, [ObjectId(self.item_id)] + ) self.assertEqual(docs.count(), 1) def test_softdelete_db_fields(self): """Documents created when soft delete is enabled should include and maintain the DELETED field in the db. """ - r = self.test_client.post(self.known_resource_url, data={ - 'ref': "1234567890123456789054321" - }) + r = self.test_client.post( + self.known_resource_url, data={"ref": "1234567890123456789054321"} + ) data, status = self.parse_response(r) self.assert201(status) - new_item_id = data[self.domain[self.known_resource]['id_field']] - new_item_etag = data[self.app.config['ETAG']] + new_item_id = data[self.domain[self.known_resource]["id_field"]] + new_item_etag = data[self.app.config["ETAG"]] with self.app.test_request_context(): db_stored_doc = self.app.data.find_one_raw( - self.known_resource, _id=ObjectId(new_item_id)) + self.known_resource, _id=ObjectId(new_item_id) + ) self.assertTrue(self.deleted_field in db_stored_doc) # PUT updates to the document should maintain the DELETED field r = self.test_client.put( self.known_resource_url + "/" + new_item_id, - data={'ref': '5432109876543210987654321'}, - headers=[('If-Match', new_item_etag)] + data={"ref": "5432109876543210987654321"}, + headers=[("If-Match", new_item_etag)], ) data, status = self.parse_response(r) self.assert200(status) - new_item_etag = data[self.app.config['ETAG']] + new_item_etag = data[self.app.config["ETAG"]] with self.app.test_request_context(): db_stored_doc = self.app.data.find_one_raw( - self.known_resource, _id=ObjectId(new_item_id)) + self.known_resource, _id=ObjectId(new_item_id) + ) self.assertTrue(self.deleted_field in db_stored_doc) # PATCH updates to the document should maintain the DELETED field r = self.test_client.patch( self.known_resource_url + "/" + new_item_id, - data={'ref': '5555544444333332222211111'}, - headers=[('If-Match', new_item_etag)] + data={"ref": "5555544444333332222211111"}, + headers=[("If-Match", new_item_etag)], ) self.assert200(r.status_code) with self.app.test_request_context(): db_stored_doc = self.app.data.find_one_raw( - self.known_resource, _id=ObjectId(new_item_id)) + self.known_resource, _id=ObjectId(new_item_id) + ) self.assertTrue(self.deleted_field in db_stored_doc) def test_exclusive_projection(self): @@ -622,7 +632,7 @@ def test_exclusive_projection(self): setting for the resource, enabling soft_deletes does not cause a 500 error. See #752. """ - r = self.test_client.get('/exclusion?show_deleted') + r = self.test_client.get("/exclusion?show_deleted") data, status = self.parse_response(r) self.assert200(status) @@ -633,34 +643,28 @@ def test_exclude_soft_deleted_documents_from_unique_checks(self): unique_value = "1234567890123456789054321" # 'ref' field has a 'unique' rule applied to it. - r = self.test_client.post(self.known_resource_url, data={ - 'ref': unique_value - }) + r = self.test_client.post(self.known_resource_url, data={"ref": unique_value}) data, status = self.parse_response(r) self.assert201(status) - new_item_id = data[self.domain[self.known_resource]['id_field']] - new_item_etag = data[self.app.config['ETAG']] + new_item_id = data[self.domain[self.known_resource]["id_field"]] + new_item_etag = data[self.app.config["ETAG"]] # we can't post a new document with the same value. - r = self.test_client.post(self.known_resource_url, data={ - 'ref': unique_value - }) + r = self.test_client.post(self.known_resource_url, data={"ref": unique_value}) data, status = self.parse_response(r) self.assert422(status) # we now soft delete the document. r = self.test_client.delete( self.known_resource_url + "/" + new_item_id, - headers=[('If-Match', new_item_etag)] + headers=[("If-Match", new_item_etag)], ) data, status = self.parse_response(r) self.assert204(status) # posting a new document with the same value for 'ref' # is now possible. - r = self.test_client.post(self.known_resource_url, data={ - 'ref': unique_value - }) + r = self.test_client.post(self.known_resource_url, data={"ref": unique_value}) data, status = self.parse_response(r) self.assert201(status) @@ -672,13 +676,13 @@ def setUp(self): # Enable soft delete for one resource domain = copy.copy(self.domain) resource_settings = domain[self.known_resource] - resource_settings['soft_delete'] = True + resource_settings["soft_delete"] = True self.app.register_resource(self.known_resource, resource_settings) - self.deleted_field = self.app.config['DELETED'] + self.deleted_field = self.app.config["DELETED"] # Etag used to delete an item (a contact) - self.etag_headers = [('If-Match', self.item_etag)] + self.etag_headers = [("If-Match", self.item_etag)] def test_resource_specific_softdelete(self): """ Resource level soft delete configuration should override @@ -695,7 +699,8 @@ def test_resource_specific_softdelete(self): # DELETE on other resources should be hard deletes data, status = self.delete( - self.invoice_id_url, headers=[('If-Match', self.invoice_etag)]) + self.invoice_id_url, headers=[("If-Match", self.invoice_etag)] + ) self.assert204(status) r = self.test_client.get(self.invoice_id_url) @@ -709,7 +714,7 @@ def test_on_pre_DELETE_for_item(self): devent = DummyEvent(self.before_delete) self.app.on_pre_DELETE += devent self.delete_item() - self.assertEqual('contacts', devent.called[0]) + self.assertEqual("contacts", devent.called[0]) self.assertFalse(devent.called[1] is None) def test_on_pre_DELETE_resource_for_item(self): @@ -733,6 +738,7 @@ def test_on_pre_DELETE_resource_for_resource(self): def test_on_pre_DELETE_dynamic_filter(self): def filter_this(resource, request, lookup): lookup["_id"] = self.unknown_item_id + self.app.on_pre_DELETE += filter_this # Would normally delete the known document; will return 404 instead. r, s = self.parse_response(self.delete_item()) @@ -768,7 +774,7 @@ def test_on_delete_resource(self): devent2 = DummyEvent(self.before_delete) self.app.on_delete_resource_originals += devent2 self.delete_resource() - self.assertEqual(('contacts',), devent1.called) + self.assertEqual(("contacts",), devent1.called) self.assertFalse(devent2.called is None) def test_on_delete_resource_contacts(self): @@ -790,30 +796,30 @@ def test_on_delete_item(self): devent = DummyEvent(self.before_delete) self.app.on_delete_item += devent self.delete_item() - self.assertEqual('contacts', devent.called[0]) - id_field = self.domain['contacts']['id_field'] + self.assertEqual("contacts", devent.called[0]) + id_field = self.domain["contacts"]["id_field"] self.assertEqual(self.item_id, str(devent.called[1][id_field])) def test_on_delete_item_contacts(self): devent = DummyEvent(self.before_delete) self.app.on_delete_item_contacts += devent self.delete_item() - id_field = self.domain['contacts']['id_field'] + id_field = self.domain["contacts"]["id_field"] self.assertEqual(self.item_id, str(devent.called[0][id_field])) def test_on_deleted_item(self): devent = DummyEvent(self.after_delete) self.app.on_deleted_item += devent self.delete_item() - self.assertEqual('contacts', devent.called[0]) - id_field = self.domain['contacts']['id_field'] + self.assertEqual("contacts", devent.called[0]) + id_field = self.domain["contacts"]["id_field"] self.assertEqual(self.item_id, str(devent.called[1][id_field])) def test_on_deleted_item_contacts(self): devent = DummyEvent(self.after_delete) self.app.on_deleted_item_contacts += devent self.delete_item() - id_field = self.domain['contacts']['id_field'] + id_field = self.domain["contacts"]["id_field"] self.assertEqual(self.item_id, str(devent.called[0][id_field])) def delete_resource(self): @@ -821,7 +827,8 @@ def delete_resource(self): def delete_item(self): return self.test_client.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) def before_delete(self): db = self.connection[MONGO_DBNAME] diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index bf2f4d10b..cb4a03e9d 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -15,58 +15,58 @@ class TestGet(TestBase): - def test_get_empty_resource(self): response, status = self.get(self.empty_resource) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 0) - links = response['_links'] + links = response["_links"] self.assertEqual(len(links), 2) self.assertResourceLink(links, self.empty_resource) self.assertHomeLink(links) def test_get_max_results(self): maxr = 10 - response, status = self.get(self.known_resource, - '?max_results=%d' % maxr) + response, status = self.get(self.known_resource, "?max_results=%d" % maxr) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), maxr) - maxr = self.app.config['PAGINATION_LIMIT'] + 1 - response, status = self.get(self.known_resource, - '?max_results=%d' % maxr) + maxr = self.app.config["PAGINATION_LIMIT"] + 1 + response, status = self.get(self.known_resource, "?max_results=%d" % maxr) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_LIMIT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_LIMIT"]) def test_get_custom_max_results(self): - self.app.config['QUERY_MAX_RESULTS'] = 'size' + self.app.config["QUERY_MAX_RESULTS"] = "size" maxr = 10 - response, status = self.get(self.known_resource, '?size=%d' % maxr) + response, status = self.get(self.known_resource, "?size=%d" % maxr) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), maxr) def test_get_custom_params(self): page = 2 - custom_params = MultiDict([('my_param', 'value1'), - ('my_param', 'value2')]) - custom_query = '&'.join('%s=%s' % (param, value) for param, values - in custom_params.lists() for value in values) - response, status = self.get(self.known_resource, - '?%s&page=%d' % (custom_query, page)) + custom_params = MultiDict([("my_param", "value1"), ("my_param", "value2")]) + custom_query = "&".join( + "%s=%s" % (param, value) + for param, values in custom_params.lists() + for value in values + ) + response, status = self.get( + self.known_resource, "?%s&page=%d" % (custom_query, page) + ) self.assert200(status) - links = response['_links'] - self.assertCustomParams(links['prev'], custom_params) - self.assertCustomParams(links['next'], custom_params) - self.assertCustomParams(links['self'], custom_params) - self.assertCustomParams(links['last'], custom_params) + links = response["_links"] + self.assertCustomParams(links["prev"], custom_params) + self.assertCustomParams(links["next"], custom_params) + self.assertCustomParams(links["self"], custom_params) + self.assertCustomParams(links["last"], custom_params) def test_get_page(self): response, status = self.get(self.known_resource) @@ -74,18 +74,18 @@ def test_get_page(self): self.assertPage(response, status) def test_get_perform_count_on_pagination_disabled(self): - self.app.config['OPTIMIZE_PAGINATION_FOR_SPEED'] = True + self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"] = True - r = self.test_client.get('%s?page=2' % self.known_resource_url) + r = self.test_client.get("%s?page=2" % self.known_resource_url) self.assert200(r.status_code) body = json.loads(r.get_data()) - links = body['_links'] - self.assertFalse('last' in links) - self.assertFalse('total' in body['_meta']) + links = body["_links"] + self.assertFalse("last" in links) + self.assertFalse("total" in body["_meta"]) self.assertNextLink(links, 3) self.assertPrevLink(links, 1) - self.assertFalse(self.app.config['HEADER_TOTAL_COUNT'] in r.headers) + self.assertFalse(self.app.config["HEADER_TOTAL_COUNT"] in r.headers) def test_get_internal_page(self): with self.app.test_request_context(self.known_resource_url): @@ -93,47 +93,47 @@ def test_get_internal_page(self): self.assertPage(response, status) def assertPage(self, response, status): - links = response['_links'] + links = response["_links"] self.assertNextLink(links, 2) self.assertLastLink(links, 5) self.assertPagination(response, 1, 101, 25) page = 1 - response, status = self.get(self.known_resource, '?page=%d' % page) + response, status = self.get(self.known_resource, "?page=%d" % page) self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertNextLink(links, 2) self.assertLastLink(links, 5) self.assertPagination(response, 1, 101, 25) page = 2 - response, status = self.get(self.known_resource, '?page=%d' % page) + response, status = self.get(self.known_resource, "?page=%d" % page) self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertNextLink(links, 3) self.assertPrevLink(links, 1) self.assertLastLink(links, 5) self.assertPagination(response, 2, 101, 25) page = 5 - response, status = self.get(self.known_resource, '?page=%d' % page) + response, status = self.get(self.known_resource, "?page=%d" % page) self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertPrevLink(links, 4) self.assertLastLink(links, None) self.assertPagination(response, 5, 101, 25) def test_get_custom_page(self): - self.app.config['QUERY_PAGE'] = 'custom' + self.app.config["QUERY_PAGE"] = "custom" page = 2 - response, status = self.get(self.known_resource, '?custom=%d' % page) + response, status = self.get(self.known_resource, "?custom=%d" % page) self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertNextLink(links, 3) self.assertPrevLink(links, 1) self.assertLastLink(links, 5) @@ -143,140 +143,139 @@ def test_get_pagination_no_documents(self): """ test that pagination meta is present even when no records are being returned. #415. """ - response, status = self.get(self.known_resource, - '?where={"ref": "not_really"}') + response, status = self.get(self.known_resource, '?where={"ref": "not_really"}') self.assert200(status) self.assertPagination(response, 1, 0, 25) def test_get_paging_disabled_no_args(self): - self.app.config['DOMAIN'][self.known_resource]['pagination'] = False + self.app.config["DOMAIN"][self.known_resource]["pagination"] = False response, status = self.get(self.known_resource) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), self.known_resource_count) - self.assertTrue(self.app.config['META'] not in response) - links = response['_links'] - self.assertTrue('next' not in links) - self.assertTrue('prev' not in links) + self.assertTrue(self.app.config["META"] not in response) + links = response["_links"] + self.assertTrue("next" not in links) + self.assertTrue("prev" not in links) def test_get_total_count_header(self): - url = self.domain[self.known_resource]['url'] + url = self.domain[self.known_resource]["url"] r = self.test_client.head(url) response, status = self.parse_response(r) self.assert200(status) self.assertEqual(response, None) - total_count = r.headers[self.app.config['HEADER_TOTAL_COUNT']] + total_count = r.headers[self.app.config["HEADER_TOTAL_COUNT"]] self.assertEqual(int(total_count), self.known_resource_count) def test_get_where_mongo_syntax(self): where = '{"ref": "%s"}' % self.item_name - response, status = self.get(self.known_resource, '?where=%s' % where) + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) def test_get_where_mongo_combined_date(self): - where = '{"$and": [{"ref": "%s"}, {"_created": \ - {"$gte": "Tue, 01 Oct 2013 00:59:22 GMT"}}]}' % self.item_name - response, status = self.get(self.known_resource, - '?where=%s' % where) + where = ( + '{"$and": [{"ref": "%s"}, {"_created": \ + {"$gte": "Tue, 01 Oct 2013 00:59:22 GMT"}}]}' + % self.item_name + ) + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) def test_get_custom_where(self): - self.app.config['QUERY_WHERE'] = 'whereas' + self.app.config["QUERY_WHERE"] = "whereas" where = '{"ref": "%s"}' % self.item_name - response, status = self.get(self.known_resource, '?whereas=%s' % where) + response, status = self.get(self.known_resource, "?whereas=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) def test_get_mongo_query_blacklist(self): - where = '{"$where": "this.ref == ''%s''"}' % self.item_name - _, status = self.get(self.known_resource, '?where=%s' % where) + where = '{"$where": "this.ref == ' "%s" '"}' % self.item_name + _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) where = '{"ref": {"$regex": "%s"}}' % self.item_name - _, status = self.get(self.known_resource, '?where=%s' % where) + _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) def test_get_mongo_query_blacklist_nested(self): - where = '{"$or": [{"$where": "this.ref == ''%s''"}]}' % self.item_name - _, status = self.get(self.known_resource, '?where=%s' % where) + where = '{"$or": [{"$where": "this.ref == ' "%s" '"}]}' % self.item_name + _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) where = '{"$or": [{"ref": {"$regex": "%s"}}]}' % self.item_name - _, status = self.get(self.known_resource, '?where=%s' % where) + _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) def test_get_where_mongo_objectid_as_string(self): where = '{"tid": "%s"}' % self.item_tid - response, status = self.get(self.known_resource, '?where=%s' % where) + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) - self.app.config['DOMAIN']['contacts']['query_objectid_as_string'] = \ - True - response, status = self.get(self.known_resource, '?where=%s' % where) + self.app.config["DOMAIN"]["contacts"]["query_objectid_as_string"] = True + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 0) def test_get_where_python_syntax(self): - where = 'ref == %s' % self.item_name - response, status = self.get(self.known_resource, '?where=%s' % where) + where = "ref == %s" % self.item_name + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) def test_get_where_python_syntax1(self): - where = 'ref == %s and _created>="Tue, 01 Oct 2013 00:59:22 GMT"' \ - % self.item_name - response, status = self.get(self.known_resource, '?where=%s' % where) + where = ( + 'ref == %s and _created>="Tue, 01 Oct 2013 00:59:22 GMT"' % self.item_name + ) + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) def test_get_query_in_links(self): """ Make sure that query strings appear in all HATEOAS links (#464). """ # find a role with enough results - for role in ('agent', 'client', 'vendor'): - where = 'role == %s' % role - response, _ = self.get(self.known_resource, '?where=%s' % where) - if response['_meta']['total'] \ - >= self.app.config['PAGINATION_DEFAULT'] + 1: + for role in ("agent", "client", "vendor"): + where = "role == %s" % role + response, _ = self.get(self.known_resource, "?where=%s" % where) + if response["_meta"]["total"] >= self.app.config["PAGINATION_DEFAULT"] + 1: break - links = response['_links'] - total = response['_meta']['total'] - max_results = response['_meta']['max_results'] + links = response["_links"] + total = response["_meta"]["total"] + max_results = response["_meta"]["max_results"] last_page = total / max_results + (1 if total % max_results else 0) - self.assertTrue('?where=%s' % where in links['self']['href']) - self.assertTrue('?where=%s' % where in links['next']['href']) - self.assertTrue('?where=%s' % where in links['last']['href']) + self.assertTrue("?where=%s" % where in links["self"]["href"]) + self.assertTrue("?where=%s" % where in links["next"]["href"]) + self.assertTrue("?where=%s" % where in links["last"]["href"]) self.assertNextLink(links, 2) self.assertLastLink(links, last_page) page = 2 - response, _ = self.get(self.known_resource, - '?where=%s&page=%d' % (where, page)) - links = response['_links'] - self.assertTrue('?where=%s' % where in links['prev']['href']) + response, _ = self.get(self.known_resource, "?where=%s&page=%d" % (where, page)) + links = response["_links"] + self.assertTrue("?where=%s" % where in links["prev"]["href"]) self.assertPrevLink(links, 1) def test_get_projection_consistent_etag(self): """ Test that #369 is fixed and projection queries return consistent etags (as they are now stored along with the document). """ - etag_field = self.app.config['ETAG'] + etag_field = self.app.config["ETAG"] data = {"inv_number": self.random_string(10)} # post a new item so etag storage kicks in @@ -285,69 +284,66 @@ def test_get_projection_consistent_etag(self): # hit the resource endpoint with a projection query projection = '{"prog": 1}' - r, status = self.get(self.empty_resource, - '?projection=%s' % projection) + r, status = self.get(self.empty_resource, "?projection=%s" % projection) # compare original etag with retrieved one - self.assertEqual(etag, r['_items'][0][etag_field]) + self.assertEqual(etag, r["_items"][0][etag_field]) def test_get_projection(self): projection = '{"prog": 1}' - response, status = self.get(self.known_resource, '?projection=%s' % - projection) + response, status = self.get(self.known_resource, "?projection=%s" % projection) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertTrue('prog' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertTrue("prog" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) projection = '{"prog": 0}' - response, status = self.get(self.known_resource, '?projection=%s' % - projection) + response, status = self.get(self.known_resource, "?projection=%s" % projection) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertFalse('prog' in r) - self.assertTrue('location' in r) - self.assertTrue('role' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("prog" in r) + self.assertTrue("location" in r) + self.assertTrue("role" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_static_projection(self): """ Test that static projections are honoured """ response, status = self.get(self.different_resource) self.assert200(status) - resource = response['_items'] + resource = response["_items"] # 'users' has a static inclusive projection with 'username' and 'ref' # fields, so other document fields should be excluded. for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertFalse('prog' in r) - self.assertTrue('username' in r) - self.assertTrue('ref' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertFalse("prog" in r) + self.assertTrue("username" in r) + self.assertTrue("ref" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_include_projection_can_exclude(self): """ Test that static projection only expose fields included @@ -355,28 +351,28 @@ def test_get_server_include_projection_can_exclude(self): """ # exclude `ref` by client side projection = '{"ref": 0}' - response, status = self.get(self.different_resource, - '?projection=%s' % - projection) + response, status = self.get( + self.different_resource, "?projection=%s" % projection + ) self.assert200(status) - resource = response['_items'] + resource = response["_items"] # 'users' has a static inclusive projection with 'username' and 'ref' # fields, so other document fields should be excluded. # and client can further exclude 'ref' or 'username'. for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertFalse('prog' in r) - self.assertTrue('username' in r) - self.assertFalse('ref' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertFalse("prog" in r) + self.assertTrue("username" in r) + self.assertFalse("ref" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_include_projection_block_sniff(self): """ Test that static projection only expose fields included @@ -384,195 +380,192 @@ def test_get_server_include_projection_block_sniff(self): """ # shouldn't work when including `prog` (excluded) by client side projection = '{"prog": 1}' - response, status = self.get(self.different_resource, - '?projection=%s' % - projection) + response, status = self.get( + self.different_resource, "?projection=%s" % projection + ) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) + self.assertFalse("location" in r) + self.assertFalse("role" in r) # shouldn't work - self.assertFalse('prog' in r) - self.assertFalse('username' in r) - self.assertFalse('ref' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertFalse("prog" in r) + self.assertFalse("username" in r) + self.assertFalse("ref" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_exclude_projection_can_project_others(self): """ Test that static projection expose fields other than excluded and support client projection on exposed fields. """ projection = '{"prog": 1, "location":1}' - response, status = self.get(self.different_resource_exclude, - '?projection=%s' % - projection) + response, status = self.get( + self.different_resource_exclude, "?projection=%s" % projection + ) self.assert200(status) - resource = response['_items'] + resource = response["_items"] # 'users' has a static inclusive projection with 'username' and 'ref' # fields, so other document fields should be excluded. # and client can further exclude 'ref' or 'username'. for r in resource: - self.assertTrue('location' in r) - self.assertFalse('role' in r) - self.assertTrue('prog' in r) - self.assertFalse('born' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertTrue("location" in r) + self.assertFalse("role" in r) + self.assertTrue("prog" in r) + self.assertFalse("born" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_exlcude_projection_can_sniff(self): """ Test that static projection expose fields other than excluded and client projection on excluded **will work**. """ projection = '{"born": 1}' - response, status = self.get(self.different_resource_exclude, - '?projection=%s' % - projection) + response, status = self.get( + self.different_resource_exclude, "?projection=%s" % projection + ) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertFalse('prog' in r) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertFalse("prog" in r) # should work - self.assertTrue('born' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertTrue("born" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_custom_projection(self): - self.app.config['QUERY_PROJECTION'] = 'view' + self.app.config["QUERY_PROJECTION"] = "view" projection = '{"prog": 1}' - response, status = self.get(self.known_resource, '?view=%s' % - projection) + response, status = self.get(self.known_resource, "?view=%s" % projection) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertTrue('prog' in r) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertTrue("prog" in r) def test_get_projection_subdocument(self): projection = '{"location.address": 1}' - response, status = self.get(self.known_resource, '?projection=%s' % - projection) + response, status = self.get(self.known_resource, "?projection=%s" % projection) self.assert200(status) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertTrue('location' in r) - self.assertTrue('address' in r['location']) - self.assertFalse('city' in r['location']) - self.assertFalse('role' in r) - self.assertFalse('prog' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + self.assertTrue("location" in r) + self.assertTrue("address" in r["location"]) + self.assertFalse("city" in r["location"]) + self.assertFalse("role" in r) + self.assertFalse("prog" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_projection_noschema(self): - self.app.config['DOMAIN'][self.known_resource]['schema'] = {} + self.app.config["DOMAIN"][self.known_resource]["schema"] = {} response, status = self.get(self.known_resource) self.assert200(status) - resource = response['_items'] + resource = response["_items"] # fields are returned anyway since no schema = return all fields for r in resource: - self.assertTrue('location' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) + self.assertTrue("location" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) def test_get_where_disabled(self): - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = [] - where = 'ref == %s' % self.item_name - response, status = self.get(self.known_resource, '?where=%s' % where) + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = [] + where = "ref == %s" % self.item_name + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) def test_get_sort_comma_delimited_syntax(self): - sort = '-prog' - response, status = self.get(self.known_resource, '?sort=%s' % sort) + sort = "-prog" + response, status = self.get(self.known_resource, "?sort=%s" % sort) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) topvalue = 100 for i in range(len(resource)): - self.assertEqual(resource[i]['prog'], topvalue - i) + self.assertEqual(resource[i]["prog"], topvalue - i) def test_get_sort_mongo_syntax(self): sort = '[("prog",-1)]' - response, status = self.get(self.known_resource, - '?sort=%s' % sort) + response, status = self.get(self.known_resource, "?sort=%s" % sort) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) topvalue = 100 for i in range(len(resource)): - self.assertEqual(resource[i]['prog'], topvalue - i) + self.assertEqual(resource[i]["prog"], topvalue - i) def test_get_custom_sort(self): - self.app.config['QUERY_SORT'] = 'orderby' + self.app.config["QUERY_SORT"] = "orderby" sort = '[("prog",-1)]' - response, status = self.get(self.known_resource, '?orderby=%s' % sort) + response, status = self.get(self.known_resource, "?orderby=%s" % sort) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) topvalue = 100 for i in range(len(resource)): - self.assertEqual(resource[i]['prog'], topvalue - i) + self.assertEqual(resource[i]["prog"], topvalue - i) def test_get_sort_disabled(self): - self.app.config['DOMAIN'][self.known_resource]['sorting'] = False + self.app.config["DOMAIN"][self.known_resource]["sorting"] = False sort = '[("prog",-1)]' - response, status = self.get(self.known_resource, '?sort=%s' % sort) + response, status = self.get(self.known_resource, "?sort=%s" % sort) self.assert200(status) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) # this might actually fail on very rare occurences as mongodb # 'natural' order is not granted to return documents in insertion order - self.assertEqual(resource[0]['prog'], 0) + self.assertEqual(resource[0]["prog"], 0) def test_get_default_sort(self): - s = self.app.config['DOMAIN'][self.known_resource]['datasource'] + s = self.app.config["DOMAIN"][self.known_resource]["datasource"] # set default sort to 'prog', desc. - s['default_sort'] = [('prog', -1)] + s["default_sort"] = [("prog", -1)] self.app.set_defaults() response, _ = self.get(self.known_resource) - self.assertEqual(response['_items'][0]['prog'], 100) + self.assertEqual(response["_items"][0]["prog"], 100) # set default sort to 'prog', asc. - s['default_sort'] = [('prog', 1)] + s["default_sort"] = [("prog", 1)] self.app.set_defaults() response, _ = self.get(self.known_resource) - self.assertEqual(response['_items'][0]['prog'], 0) + self.assertEqual(response["_items"][0]["prog"], 0) def test_cache_control(self): self.assertCacheControl(self.known_resource_url) @@ -594,19 +587,19 @@ def test_get_same_collection_different_resource(self): response, status = self.get(self.different_resource) self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertEqual(len(links), 2) self.assertHomeLink(links) self.assertResourceLink(links, self.different_resource) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 2) for item in resource: # 'user' title instead of original 'contact' self.assertItem(item, self.different_resource) - etag = item.get(self.app.config['ETAG']) + etag = item.get(self.app.config["ETAG"]) self.assertTrue(etag is not None) def test_documents_missing_standard_date_fields(self): @@ -614,170 +607,158 @@ def test_documents_missing_standard_date_fields(self): LAST_UPDATED and/or DATE_CREATED fields. """ contacts = self.random_contacts(1, False) - ref = 'test_update_field' - contacts[0]['ref'] = ref + ref = "test_update_field" + contacts[0]["ref"] = ref _db = self.connection[MONGO_DBNAME] _db.contacts.insert_one(contacts[0]) where = '{"ref": "%s"}' % ref - response, status = self.get(self.known_resource, - '?where=%s' % where) + response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) - resource = response['_items'] + resource = response["_items"] self.assertEqual(len(resource), 1) self.assertItem(resource[0], self.known_resource) def test_get_where_allowed_filters(self): - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['notreally'] + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = [ + "notreally" + ] where = '{"ref": "%s"}' % self.item_name - r = self.test_client.get('%s%s' % (self.known_resource_url, - '?where=%s' % where)) + r = self.test_client.get( + "%s%s" % (self.known_resource_url, "?where=%s" % where) + ) self.assert400(r.status_code) self.assertTrue(b"'ref' not allowed" in r.get_data()) - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['*'] - r = self.test_client.get('%s%s' % (self.known_resource_url, - '?where=%s' % where)) + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = ["*"] + r = self.test_client.get( + "%s%s" % (self.known_resource_url, "?where=%s" % where) + ) self.assert200(r.status_code) # `allowed_filters` contains "rows" --> filter key "rows.price" # must be allowed - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['rows'] + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = ["rows"] where = '{"rows.price": 10}' - r = self.test_client.get('%s%s' % (self.known_resource_url, - '?where=%s' % where)) + r = self.test_client.get( + "%s%s" % (self.known_resource_url, "?where=%s" % where) + ) self.assert200(r.status_code) # `allowed_filters` contains "rows.price" --> filter key "rows.price" # must be allowed - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['rows.price'] - r = self.test_client.get('%s%s' % (self.known_resource_url, - '?where=%s' % where)) + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = [ + "rows.price" + ] + r = self.test_client.get( + "%s%s" % (self.known_resource_url, "?where=%s" % where) + ) self.assert200(r.status_code) # `allowed_filters` contains "rows.price" --> filter key "rows" # must NOT be allowed where = '{"rows": {"sku": "value", "price": 10}}' - r = self.test_client.get('%s%s' % (self.known_resource_url, - '?where=%s' % where)) + r = self.test_client.get( + "%s%s" % (self.known_resource_url, "?where=%s" % where) + ) self.assert400(r.status_code) self.assertTrue(b"'rows' not allowed" in r.get_data()) def test_get_with_post_override(self): # POST request with GET override turns into a GET - headers = [('X-HTTP-Method-Override', 'GET')] + headers = [("X-HTTP-Method-Override", "GET")] r = self.test_client.post(self.known_resource_url, headers=headers) response, status = self.parse_response(r) self.assertGet(response, status) def test_get_custom_items(self): - self.app.config['ITEMS'] = '_documents' + self.app.config["ITEMS"] = "_documents" response, _ = self.get(self.known_resource) - self.assertTrue('_documents' in response and '_items' not in response) + self.assertTrue("_documents" in response and "_items" not in response) def test_get_custom_links(self): - self.app.config['LINKS'] = '_navigation' + self.app.config["LINKS"] = "_navigation" response, _ = self.get(self.known_resource) - self.assertTrue('_navigation' in response and '_links' not in response) + self.assertTrue("_navigation" in response and "_links" not in response) def test_get_custom_hateoas_links(self): def change_links(response): - response['_links'] = {'self': {'title': 'Custom', - 'href': '/custom/1'}} + response["_links"] = {"self": {"title": "Custom", "href": "/custom/1"}} + self.app.on_fetched_resource_contacts += change_links response, _ = self.get(self.known_resource) - self.assertTrue('Custom' in response['_links']['self']['title']) - self.assertTrue('/custom/1' in response['_links']['self']['href']) + self.assertTrue("Custom" in response["_links"]["self"]["title"]) + self.assertTrue("/custom/1" in response["_links"]["self"]["href"]) def test_get_custom_auto_document_fields(self): - self.app.config['LAST_UPDATED'] = '_updated_on' - self.app.config['DATE_CREATED'] = '_created_on' - self.app.config['ETAG'] = '_the_etag' + self.app.config["LAST_UPDATED"] = "_updated_on" + self.app.config["DATE_CREATED"] = "_created_on" + self.app.config["ETAG"] = "_the_etag" response, _ = self.get(self.known_resource) - for document in response['_items']: - self.assertTrue('_updated_on' in document) - self.assertTrue('_created_on' in document) - self.assertTrue('_the_etag' in document) + for document in response["_items"]: + self.assertTrue("_updated_on" in document) + self.assertTrue("_created_on" in document) + self.assertTrue("_the_etag" in document) def test_get_embedded_media_validate_rest_of_fields(self): """ test multipart/form-data resource fields that are JSON encoded are validated correctly. #806 """ - self.app.config['MULTIPART_FORM_FIELDS_AS_JSON'] = True + self.app.config["MULTIPART_FORM_FIELDS_AS_JSON"] = True resource_with_media = { - 'image_file': { - 'type': 'media' - }, - 'some_text': { - 'type': 'string' - }, - 'some_boolean': { - 'type': 'boolean' - }, - 'some_number': { - 'type': 'number' - }, - 'some_list': { - 'type': 'list', - 'schema': {'type': 'string'} - } + "image_file": {"type": "media"}, + "some_text": {"type": "string"}, + "some_boolean": {"type": "boolean"}, + "some_number": {"type": "number"}, + "some_list": {"type": "list", "schema": {"type": "string"}}, } - self.app.register_resource('res_img', {'schema': resource_with_media}) + self.app.register_resource("res_img", {"schema": resource_with_media}) - img = b'some_image' + img = b"some_image" # fail on boolean validate - data = {'image_file': (BytesIO(img), 'test.txt'), - 'some_boolean': '123' - } + data = {"image_file": (BytesIO(img), "test.txt"), "some_boolean": "123"} response, status = self.parse_response( - self.test_client.post("res_img", - data=data, - headers=[('Content-Type', - 'multipart/form-data')])) + self.test_client.post( + "res_img", data=data, headers=[("Content-Type", "multipart/form-data")] + ) + ) self.assert422(status) # fail on number validattion - data = {'image_file': (BytesIO(img), 'test.txt'), - 'some_number': 'xyz' - } + data = {"image_file": (BytesIO(img), "test.txt"), "some_number": "xyz"} response, status = self.parse_response( - self.test_client.post("res_img", - data=data, - headers=[('Content-Type', - 'multipart/form-data')])) + self.test_client.post( + "res_img", data=data, headers=[("Content-Type", "multipart/form-data")] + ) + ) self.assert422(status) # fail on list validation - data = {'image_file': (BytesIO(img), 'test.txt'), - 'some_list': "true" - } + data = {"image_file": (BytesIO(img), "test.txt"), "some_list": "true"} response, status = self.parse_response( - self.test_client.post("res_img", - data=data, - headers=[('Content-Type', - 'multipart/form-data')])) + self.test_client.post( + "res_img", data=data, headers=[("Content-Type", "multipart/form-data")] + ) + ) self.assert422(status) # validate all fields correctly - data = {'image_file': (BytesIO(img), 'test.txt'), - 'some_text': '"abc"', - 'some_boolean': 'true', - 'some_number': '123', - 'some_list': "[\"abc\", \"xyz\"]" - } + data = { + "image_file": (BytesIO(img), "test.txt"), + "some_text": '"abc"', + "some_boolean": "true", + "some_number": "123", + "some_list": '["abc", "xyz"]', + } response, status = self.parse_response( - self.test_client.post("res_img", - data=data, - headers=[('Content-Type', - 'multipart/form-data')])) + self.test_client.post( + "res_img", data=data, headers=[("Content-Type", "multipart/form-data")] + ) + ) self.assert201(status) - self.app.config['MULTIPART_FORM_FIELDS_AS_JSON'] = False + self.app.config["MULTIPART_FORM_FIELDS_AS_JSON"] = False def test_get_embedded_media(self): """ test that embeedded images are properly rendered and #305 is fixed. @@ -785,56 +766,59 @@ def test_get_embedded_media(self): # add a 'digital_assets' endpoint to the API self.app.register_resource( - 'digital_assets', - {'schema': {'file': {'type': 'media'}}} + "digital_assets", {"schema": {"file": {"type": "media"}}} ) # add an 'images' endpoint to the API. this will expose the embedded # digital assets images = { - 'image_file': { - 'type': 'objectid', - 'data_relation': { - 'resource': 'digital_assets', - 'field': '_id', - 'embeddable': True - } + "image_file": { + "type": "objectid", + "data_relation": { + "resource": "digital_assets", + "field": "_id", + "embeddable": True, + }, } } - self.app.register_resource('images', {'schema': images}) + self.app.register_resource("images", {"schema": images}) # post an asset - asset = b'a_file' - data = {'file': (BytesIO(asset), 'test.txt')} + asset = b"a_file" + data = {"file": (BytesIO(asset), "test.txt")} response, status = self.parse_response( - self.test_client.post("digital_assets", - data=data, - headers=[('Content-Type', - 'multipart/form-data')])) + self.test_client.post( + "digital_assets", + data=data, + headers=[("Content-Type", "multipart/form-data")], + ) + ) self.assert201(status) # post a document to the 'images' endpoint. the document is referencing # the newly posted digital asset. - data = {'image_file': ObjectId(response['_id'])} + data = {"image_file": ObjectId(response["_id"])} response, status = self.parse_response( - self.test_client.post("images", data=data)) + self.test_client.post("images", data=data) + ) self.assert201(status) # retrieve the document from the same endpoint, requesting for the # digital asset to be embedded within the retrieved document - image_id = response['_id'] + image_id = response["_id"] response, status = self.parse_response( self.test_client.get( - '%s/%s%s' % ('images', image_id, - '?embedded={"image_file": 1}'))) + "%s/%s%s" % ("images", image_id, '?embedded={"image_file": 1}') + ) + ) self.assert200(status) # test that the embedded document contains the same data as orignially # posted on the digital_asset endpoint. - returned = response['image_file']['file'] + returned = response["image_file"]["file"] # encodedstring will raise a DeprecationWarning under Python3.3, but # the alternative encodebytes is not available in Python 2. - encoded = base64.encodestring(asset).decode('utf-8') + encoded = base64.encodestring(asset).decode("utf-8") self.assertEqual(returned, encoded) self.assertEqual(base64.decodestring(returned.encode()), asset) @@ -844,227 +828,232 @@ def test_get_embedded(self): fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) - invoices = self.domain['invoices'] + invoices = self.domain["invoices"] # Test that we get 400 if can't parse dict - embedded = 'not-a-dict' - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + embedded = "not-a-dict" + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert400(r.status_code) # Test that doesn't come embedded if asking for a field that # isn't embedded (global setting is False by default) embedded = '{"person": 1}' - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertEqual(content['_items'][0]['person'], str(fake_contact_id)) + self.assertEqual(content["_items"][0]["person"], str(fake_contact_id)) # Set field to be embedded - invoices['schema']['person']['data_relation']['embeddable'] = True + invoices["schema"]["person"]["data_relation"]["embeddable"] = True # Test that global setting applies even if field is set to embedded - invoices['embedding'] = False - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + invoices["embedding"] = False + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertEqual(content['_items'][0]['person'], str(fake_contact_id)) + self.assertEqual(content["_items"][0]["person"], str(fake_contact_id)) # Test that it works - invoices['embedding'] = True - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + invoices["embedding"] = True + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['_items'][0]['person']) + self.assertTrue("location" in content["_items"][0]["person"]) # Test that it ignores a bogus field embedded = '{"person": 1, "not-a-real-field": 1}' - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['_items'][0]['person']) + self.assertTrue("location" in content["_items"][0]["person"]) # Test that it ignores a real field with a bogus value embedded = '{"person": 1, "inv_number": "not-a-real-value"}' - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['_items'][0]['person']) + self.assertTrue("location" in content["_items"][0]["person"]) # Test that it works with item endpoint too - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['person']) + self.assertTrue("location" in content["person"]) # Add new embeddable field to schema - invoices['schema']['missing-field'] = { - 'type': 'objectid', - 'data_relation': {'resource': 'contacts', 'embeddable': True} + invoices["schema"]["missing-field"] = { + "type": "objectid", + "data_relation": {"resource": "contacts", "embeddable": True}, } # Test that it ignores embeddable field that is missing from document embedded = '{"missing-field": 1}' - r = self.test_client.get('%s/%s' % (invoices['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertFalse('missing-field' in content['_items'][0]) + self.assertFalse("missing-field" in content["_items"][0]) # Test default fields to be embedded - invoices['embedded_fields'] = ['person'] - r = self.test_client.get("%s/" % invoices['url']) + invoices["embedded_fields"] = ["person"] + r = self.test_client.get("%s/" % invoices["url"]) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['_items'][0]['person']) + self.assertTrue("location" in content["_items"][0]["person"]) # Test that default fields are overwritten by ?embedded=...0 embedded = '{"person": 0}' - r = self.test_client.get("%s/%s" % (invoices['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertFalse('location' in content['_items'][0]['person']) + self.assertFalse("location" in content["_items"][0]["person"]) def test_get_custom_embedded(self): - self.app.config['QUERY_EMBEDDED'] = 'included' + self.app.config["QUERY_EMBEDDED"] = "included" # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) - invoices = self.domain['invoices'] - invoices['schema']['person']['data_relation']['embeddable'] = True + invoices = self.domain["invoices"] + invoices["schema"]["person"]["data_relation"]["embeddable"] = True # Test that doesn't come embedded if asking for a field that # isn't embedded (global setting is False by default) embedded = '{"person": 1}' - invoices['embedding'] = True - r = self.test_client.get('%s/%s' % (invoices['url'], - '?included=%s' % embedded)) + invoices["embedding"] = True + r = self.test_client.get("%s/%s" % (invoices["url"], "?included=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['_items'][0]['person']) + self.assertTrue("location" in content["_items"][0]["person"]) def test_get_reference_embedded_in_subdocuments(self): _db = self.connection[MONGO_DBNAME] holding_contacts = self.random_contacts(2) - holding_contact_ids = \ - _db.contacts.insert_many(holding_contacts).inserted_ids + holding_contact_ids = _db.contacts.insert_many(holding_contacts).inserted_ids contacts = self.random_contacts(2) contact_ids = _db.contacts.insert_many(contacts).inserted_ids - holding = {'departments': [{'title': 'managment', - 'members': holding_contact_ids}]} + holding = { + "departments": [{"title": "managment", "members": holding_contact_ids}] + } holding_id = _db.companies.insert_one(holding).inserted_id - company = {'holding': holding_id, - 'departments': [{'title': 'development', - 'members': contact_ids}]} + company = { + "holding": holding_id, + "departments": [{"title": "development", "members": contact_ids}], + } company_id = _db.companies.insert_one(company).inserted_id # Add a documents with no reference that should be ignored _db.companies.insert_one({}) - _db.companies.insert_one({'departments': []}) + _db.companies.insert_one({"departments": []}) - companies = self.domain['companies'] + companies = self.domain["companies"] contact_ids = list(map(str, contact_ids)) # Test that doesn't come embedded if asking for a field that # isn't embedded ('embeddable' is False by default) embedded = ( - '{"departments.members": 1,' + - ' "holding": 1, "holding.departments.members": 1}') - r = self.test_client.get('%s/%s' % (companies['url'], - '?embedded=%s' % embedded)) + '{"departments.members": 1,' + + ' "holding": 1, "holding.departments.members": 1}' + ) + r = self.test_client.get( + "%s/%s" % (companies["url"], "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertEqual(content['_items'][1]['departments'][0]['members'], - contact_ids) + self.assertEqual(content["_items"][1]["departments"][0]["members"], contact_ids) # Set field to be embedded - department_def = companies['schema']['departments']['schema'] - member_def = department_def['schema']['members']['schema'] - member_def['data_relation']['embeddable'] = True - companies['schema']['holding']['data_relation']['embeddable'] = True + department_def = companies["schema"]["departments"]["schema"] + member_def = department_def["schema"]["members"]["schema"] + member_def["data_relation"]["embeddable"] = True + companies["schema"]["holding"]["data_relation"]["embeddable"] = True # Test that global setting applies even if field is set to embedded - companies['embedding'] = False - r = self.test_client.get('%s/%s' % (companies['url'], - '?embedded=%s' % embedded)) + companies["embedding"] = False + r = self.test_client.get( + "%s/%s" % (companies["url"], "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertEqual(content['_items'][1]['departments'][0]['members'], - contact_ids) + self.assertEqual(content["_items"][1]["departments"][0]["members"], contact_ids) # Test that it works - companies['embedding'] = True - r = self.test_client.get('%s/%s' % (companies['url'], - '?embedded=%s' % embedded)) + companies["embedding"] = True + r = self.test_client.get( + "%s/%s" % (companies["url"], "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in - content['_items'][0]['departments'][0]['members'][0]) + self.assertTrue( + "location" in content["_items"][0]["departments"][0]["members"][0] + ) # Test that the second company is associated with the holding - self.assertTrue('location' in - content['_items'][1]['holding'] - ['departments'][0]['members'][0]) + self.assertTrue( + "location" + in content["_items"][1]["holding"]["departments"][0]["members"][0] + ) # Test that it ignores a bogus field embedded = '{"departments.members": 1, "not-a-real-field": 1}' - r = self.test_client.get('%s/%s' % (companies['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s" % (companies["url"], "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in - content['_items'][0]['departments'][0]['members'][0]) + self.assertTrue( + "location" in content["_items"][0]["departments"][0]["members"][0] + ) # Test that it works with item endpoint too embedded = '{"departments.members": 1}' - r = self.test_client.get('%s/%s/%s' % (companies['url'], company_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (companies["url"], company_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['departments'][0]['members'][0]) + self.assertTrue("location" in content["departments"][0]["members"][0]) # Test default fields to be embedded - companies['embedded_fields'] = ["departments.members"] - r = self.test_client.get('%s/' % companies['url']) + companies["embedded_fields"] = ["departments.members"] + r = self.test_client.get("%s/" % companies["url"]) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in - content['_items'][0]['departments'][0]['members'][0]) + self.assertTrue( + "location" in content["_items"][0]["departments"][0]["members"][0] + ) # Test that default fields are overwritten by ?embedded=...0 embedded = '{"departments.members": 0}' - r = self.test_client.get('%s/%s' % (companies['url'], - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s" % (companies["url"], "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertFalse('location' in - content['_items'][0]['departments'][0]['members'][0]) + self.assertFalse( + "location" in content["_items"][0]["departments"][0]["members"][0] + ) def test_get_nested_resource(self): - response, status = self.get('users/overseas') - self.assertGet(response, status, 'users_overseas') + response, status = self.get("users/overseas") + self.assertGet(response, status, "users_overseas") def test_cursor_extra_find(self): _find = self.app.data.find - hits = {'total_hits': 0} + hits = {"total_hits": 0} def find(resource, req, sub_resource_lookup): def extra(response): - response['_hits'] = hits + response["_hits"] = hits + cursor = _find(resource, req, sub_resource_lookup) cursor.extra = extra return cursor @@ -1072,20 +1061,19 @@ def extra(response): self.app.data.find = find r, status = self.get(self.known_resource) self.assert200(status) - self.assertTrue('_hits' in r) - self.assertEqual(r['_hits'], hits) + self.assertTrue("_hits" in r) + self.assertEqual(r["_hits"], hits) def test_get_resource_title(self): # test that resource endpoints accepts custom titles. - self.app.config['DOMAIN'][self.known_resource]['resource_title'] = \ - 'new title' + self.app.config["DOMAIN"][self.known_resource]["resource_title"] = "new title" response, _ = self.get(self.known_resource) - self.assertTrue('new title' in response['_links']['self']['title']) + self.assertTrue("new title" in response["_links"]["self"]["title"]) # test that the home page accepts custom titles. - response, _ = self.get('/') + response, _ = self.get("/") found = False - for link in response['_links']['child']: - if link['title'] == 'new title': + for link in response["_links"]["child"]: + if link["title"] == "new title": found = True break self.assertTrue(found) @@ -1097,26 +1085,27 @@ def test_get_subresource(self): fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices' % fake_contact_id) + response, status = self.get("users/%s/invoices" % fake_contact_id) self.assert200(status) # only 1 invoice - self.assertEqual(len(response['_items']), 1) - self.assertEqual(len(response['_links']), 2) + self.assertEqual(len(response["_items"]), 1) + self.assertEqual(len(response["_links"]), 2) # which links to the right contact - self.assertEqual(response['_items'][0]['person'], str(fake_contact_id)) + self.assertEqual(response["_items"][0]["person"], str(fake_contact_id)) def test_get_ifmatch_disabled(self): # when IF_MATCH is disabled no etag is present in payload - self.app.config['IF_MATCH'] = False + self.app.config["IF_MATCH"] = False response, status = self.get(self.known_resource) - resource = response['_items'] + resource = response["_items"] for r in resource: - self.assertTrue(self.app.config['ETAG'] not in r) + self.assertTrue(self.app.config["ETAG"] not in r) def test_get_ims_empty_resource(self): # test that a GET with a If-Modified-Since on an empty resource does @@ -1124,36 +1113,37 @@ def test_get_ims_empty_resource(self): # get the resource and retrieve its IMS. r = self.test_client.get(self.known_resource_url) - last_modified = r.headers.get('Last-Modified') + last_modified = r.headers.get("Last-Modified") # delete the whole resource content. r = self.test_client.delete(self.known_resource_url) # send a get with a IMS header from previous GET. - r = self.test_client.get(self.known_resource_url, - headers=[('If-Modified-Since', - last_modified)]) + r = self.test_client.get( + self.known_resource_url, headers=[("If-Modified-Since", last_modified)] + ) self.assert200(r.status_code) - self.assertEqual(json.loads(r.get_data())['_items'], []) + self.assertEqual(json.loads(r.get_data())["_items"], []) def test_get_idfield_doesnt_exist(self): # test that a non-existing id field will be silently handled when # building HATEOAS document link (#351). - self.domain[self.known_resource]['id_field'] = 'id' + self.domain[self.known_resource]["id_field"] = "id" response, status = self.get(self.known_resource) self.assert200(status) def test_get_invalid_idfield_cors(self): """ test that #381 is fixed. """ - request = '/%s/badid' % self.known_resource - self.app.config['X_DOMAINS'] = '*' - r = self.test_client.get(request, headers=[('Origin', 'test.com')]) + request = "/%s/badid" % self.known_resource + self.app.config["X_DOMAINS"] = "*" + r = self.test_client.get(request, headers=[("Origin", "test.com")]) self.assert404(r.status_code) def test_get_invalid_where_syntax(self): """ test that 'where' syntax with unknown '$' operator returns 400. """ - response, status = self.get(self.known_resource, - '?where={"field": {"$foo": "bar"}}') + response, status = self.get( + self.known_resource, '?where={"field": {"$foo": "bar"}}' + ) self.assert400(status) def test_get_invalid_sort_syntax(self): @@ -1166,41 +1156,50 @@ def test_get_allowed_filters_operators(self): (#388). Also, test that nested filters are validated. """ where = '?where={"$and": [{"field1": "value1"}, {"field2": "value2"}]}' - settings = self.app.config['DOMAIN'][self.known_resource] + settings = self.app.config["DOMAIN"][self.known_resource] # valid - settings['allowed_filters'] = ['field1', 'field2'] + settings["allowed_filters"] = ["field1", "field2"] response, status = self.get(self.known_resource, where) self.assert200(status) # invalid - settings['allowed_filters'] = ['field2'] + settings["allowed_filters"] = ["field2"] response, status = self.get(self.known_resource, where) self.assert400(status) def test_get_nested_filter_operators_unvalidated(self): """ test that nested filter operators are working correctly. """ - where = ''.join( - ('?where={"$and":[{"$or":[{"fldA":"valA"},', - '{"fldB":"valB"}]},{"fld2":"val2"}]}')) + where = "".join( + ( + '?where={"$and":[{"$or":[{"fldA":"valA"},', + '{"fldB":"valB"}]},{"fld2":"val2"}]}', + ) + ) response, status = self.get(self.known_resource, where) self.assert200(status) def test_get_nested_filter_operators_validated(self): """ test that nested filter operators are working correctly. """ - self.app.config['VALIDATE_FILTERS'] = True + self.app.config["VALIDATE_FILTERS"] = True - where = ''.join( - ('?where={"$and":[{"$or":[{"fldA":"valA"},', - '{"fldB":"valB"}]},{"fld2":"val2"}]}')) + where = "".join( + ( + '?where={"$and":[{"$or":[{"fldA":"valA"},', + '{"fldB":"valB"}]},{"fld2":"val2"}]}', + ) + ) response, status = self.get(self.known_resource, where) self.assert400(status) - where = ''.join( - ('?where={"$and":[{"$or":[{"role":', - '["agent","client"]},{"key1":"str"}]}, {"prog":1}]}')) + where = "".join( + ( + '?where={"$and":[{"$or":[{"role":', + '["agent","client"]},{"key1":"str"}]}, {"prog":1}]}', + ) + ) response, status = self.get(self.known_resource, where) self.assert200(status) @@ -1208,7 +1207,7 @@ def test_get_invalid_where_fields(self): """ test that checks all fields of the where clause to be valid resource fields according to the resource schema. """ - self.app.config['VALIDATE_FILTERS'] = True + self.app.config["VALIDATE_FILTERS"] = True # test for an outright missing/invalid field present where = '?where={"$and": [{"bad_field": "val"}, {"fld2": "val2"}]}' @@ -1266,44 +1265,42 @@ def test_get_lookup_field_as_string(self): # of string type and which value is castable to a ObjectId is still # treated as a string when 'query_objectid_as_string' is set to True. # See PR #552. - data = {'id': '507c7f79bcf86cd7994f6c0e', 'name': 'john'} - response, status = self.post('ids', data=data) + data = {"id": "507c7f79bcf86cd7994f6c0e", "name": "john"} + response, status = self.post("ids", data=data) self.assert201(status) where = '?where={"id": "507c7f79bcf86cd7994f6c0e"}' - response, status = self.get('ids', where) + response, status = self.get("ids", where) self.assert200(status) - items = response['_items'] + items = response["_items"] self.assertEqual(1, len(items)) def test_get_custom_idfield(self): - response, status = self.get('products') + response, status = self.get("products") self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertEqual(2, len(links)) self.assertHomeLink(links) - self.assertResourceLink(links, 'products') - items = response['_items'] + self.assertResourceLink(links, "products") + items = response["_items"] self.assertEqual(10, len(items)) for item in items: - self.assertItem(item, 'products') + self.assertItem(item, "products") def test_get_subresource_with_custom_idfield(self): db = self.connection[MONGO_DBNAME] - parent_product_sku = db.products.find_one()['sku'] + parent_product_sku = db.products.find_one()["sku"] product = { - 'sku': 'BAZ', - 'title': 'Child product', - 'parent_product': parent_product_sku + "sku": "BAZ", + "title": "Child product", + "parent_product": parent_product_sku, } db.products.insert_one(product) - response, status = self.get('products/%s/children' % - parent_product_sku) + response, status = self.get("products/%s/children" % parent_product_sku) self.assert200(status) - self.assertEqual(len(response['_items']), 1) - self.assertEqual(len(response['_links']), 2) - self.assertEqual(response['_items'][0]['parent_product'], - parent_product_sku) + self.assertEqual(len(response["_items"]), 1) + self.assertEqual(len(response["_links"]), 2) + self.assertEqual(response["_items"][0]["parent_product"], parent_product_sku) def test_get_aggregation_endpoint(self): @@ -1313,7 +1310,7 @@ def test_get_aggregation_endpoint(self): {"x": 1, "tags": ["dog", "cat"]}, {"x": 2, "tags": ["cat"]}, {"x": 2, "tags": ["mouse", "cat", "dog"]}, - {"x": 3, "tags": []} + {"x": 3, "tags": []}, ] ) @@ -1321,61 +1318,61 @@ def test_get_aggregation_endpoint(self): self.app.before_aggregation += self.devent self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [ {"$unwind": "$tags"}, - {"$group": {"_id": "$tags", "count": {"$sum": - "$field1"}}}, - {"$sort": SON([("count", -1), ("_id", -1)])} - ], + {"$group": {"_id": "$tags", "count": {"$sum": "$field1"}}}, + {"$sort": SON([("count", -1), ("_id", -1)])}, + ] } } - } + }, ) - response, status = self.get('aggregate_test?aggregate=ciao') + response, status = self.get("aggregate_test?aggregate=ciao") self.assert400(status) self.assertTrue(self.devent.called is None) def assertOutput(doc, count, id): - self.assertEqual(doc['count'], count) - self.assertEqual(doc['_id'], id) + self.assertEqual(doc["count"], count) + self.assertEqual(doc["_id"], id) response, status = self.get('aggregate_test?aggregate={"$field1":1}') self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 3) - assertOutput(docs[0], 3, 'cat') - assertOutput(docs[1], 2, 'dog') - assertOutput(docs[2], 1, 'mouse') - self.assertEqual('aggregate_test', self.devent.called[0]) + assertOutput(docs[0], 3, "cat") + assertOutput(docs[1], 2, "dog") + assertOutput(docs[2], 1, "mouse") + self.assertEqual("aggregate_test", self.devent.called[0]) response, status = self.get('aggregate_test?aggregate={"$field1":2}') self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 3) - assertOutput(docs[0], 6, 'cat') - assertOutput(docs[1], 4, 'dog') - assertOutput(docs[2], 2, 'mouse') - self.assertEqual('aggregate_test', self.devent.called[0]) + assertOutput(docs[0], 6, "cat") + assertOutput(docs[1], 4, "dog") + assertOutput(docs[2], 2, "mouse") + self.assertEqual("aggregate_test", self.devent.called[0]) # this will return 0 for all documents 'count' fields as no $field1 # will be gien with the query (actually, no query will be there at all) - response, status = self.get('aggregate_test') + response, status = self.get("aggregate_test") self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 3) - self.assertEqual(docs[0]['count'], 0) - self.assertEqual(docs[1]['count'], 0) - self.assertEqual(docs[2]['count'], 0) - self.assertEqual('aggregate_test', self.devent.called[0]) + self.assertEqual(docs[0]["count"], 0) + self.assertEqual(docs[1]["count"], 0) + self.assertEqual(docs[2]["count"], 0) + self.assertEqual("aggregate_test", self.devent.called[0]) # malformed field name is ignored response, status = self.get('aggregate_test?aggregate={"field1":1}') self.assert200(status) - self.assertEqual('aggregate_test', self.devent.called[0]) + self.assertEqual("aggregate_test", self.devent.called[0]) # unknown field is ignored response, status = self.get('aggregate_test?aggregate={"$unknown":1}') @@ -1396,30 +1393,30 @@ def test_get_aggregation_parsing(self): ) self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ - {"$match": {"date": {"$gte": "$date"}}} - ], + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [{"$match": {"date": {"$gte": "$date"}}}] } } - } + }, ) - challenge = date.strftime(self.app.config['DATE_FORMAT']) - response, status = self.get('aggregate_test?aggregate={"$date": "%s"}' - % challenge) + challenge = date.strftime(self.app.config["DATE_FORMAT"]) + response, status = self.get( + 'aggregate_test?aggregate={"$date": "%s"}' % challenge + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 3) - challenge = (date + timedelta(days=-1)).strftime( - self.app.config['DATE_FORMAT']) - response, status = self.get('aggregate_test?aggregate={"$date": "%s"}' - % challenge) + challenge = (date + timedelta(days=-1)).strftime(self.app.config["DATE_FORMAT"]) + response, status = self.get( + 'aggregate_test?aggregate={"$date": "%s"}' % challenge + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 4) def test_get_aggregation_with_lists(self): @@ -1434,137 +1431,132 @@ def test_get_aggregation_with_lists(self): ) self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [ { "$match": { - "$or": [ - {"tags": "$match_tags"}, - {"x": ["$x"]} - ] + "$or": [{"tags": "$match_tags"}, {"x": ["$x"]}] } } ] } } - } + }, ) - response, status = self.get( - 'aggregate_test?aggregate={"$match_tags": "a"}') + response, status = self.get('aggregate_test?aggregate={"$match_tags": "a"}') self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 3) response, status = self.get( - 'aggregate_test?aggregate={"$match_tags": ["a", "b"]}') + 'aggregate_test?aggregate={"$match_tags": ["a", "b"]}' + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 1) response, status = self.get('aggregate_test?aggregate={"$x": 4}') self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 1) def test_get_aggregation_pagination(self): _db = self.connection[MONGO_DBNAME] num = 75 - _db.aggregate_test.insert_many([{'x': x} for x in range(num)]) + _db.aggregate_test.insert_many([{"x": x} for x in range(num)]) self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ - {"$sort": SON([("x", -1)])} - ], - } + "aggregate_test", + { + "datasource": { + "aggregation": {"pipeline": [{"$sort": SON([("x", -1)])}]} } - } + }, ) # first page - response, status = self.get('aggregate_test') + response, status = self.get("aggregate_test") self.assert200(status) - items = response['_items'] - expected_length = self.app.config['PAGINATION_DEFAULT'] + items = response["_items"] + expected_length = self.app.config["PAGINATION_DEFAULT"] self.assertEqual(len(items), expected_length) item, value = 0, num - 1 - self.assertEqual(items[item]['x'], value) + self.assertEqual(items[item]["x"], value) item, value = expected_length - 1, num - expected_length - self.assertEqual(items[item]['x'], value) + self.assertEqual(items[item]["x"], value) # second page - response, status = self.get('aggregate_test?page=2') + response, status = self.get("aggregate_test?page=2") self.assert200(status) - items = response['_items'] - expected_length = self.app.config['PAGINATION_DEFAULT'] + items = response["_items"] + expected_length = self.app.config["PAGINATION_DEFAULT"] self.assertEqual(len(items), expected_length) - item, value = 0, num - 1 - self.app.config['PAGINATION_DEFAULT'] - self.assertEqual(items[item]['x'], value) + item, value = 0, num - 1 - self.app.config["PAGINATION_DEFAULT"] + self.assertEqual(items[item]["x"], value) item, value = expected_length - 1, num - expected_length * 2 - self.assertEqual(items[item]['x'], value) + self.assertEqual(items[item]["x"], value) # third page - response, status = self.get('aggregate_test?page=3') + response, status = self.get("aggregate_test?page=3") self.assert200(status) - items = response['_items'] - expected_length = num - self.app.config['PAGINATION_DEFAULT'] * 2 + items = response["_items"] + expected_length = num - self.app.config["PAGINATION_DEFAULT"] * 2 self.assertEqual(len(items), expected_length) item, value = 0, expected_length - 1 - self.assertEqual(items[item]['x'], value) + self.assertEqual(items[item]["x"], value) item, value = expected_length - 1, 0 - self.assertEqual(items[item]['x'], 0) + self.assertEqual(items[item]["x"], 0) # pagination is disabled for the endpoint - self.domain['aggregate_test']['pagination'] = False + self.domain["aggregate_test"]["pagination"] = False # hence we get all documents with a single request - response, status = self.get('aggregate_test') + response, status = self.get("aggregate_test") self.assert200(status) - items = response['_items'] + items = response["_items"] self.assertEqual(len(items), num) # and pagination requests are ignored - response, status = self.get('aggregate_test?page=2') + response, status = self.get("aggregate_test?page=2") self.assert200(status) - items = response['_items'] + items = response["_items"] self.assertEqual(len(items), num) def test_get_query_bitwise_query_operators(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) response, status = self.delete(self.known_resource_url) self.assert204(status) - data = {'prog': 20} # 00010100 + data = {"prog": 20} # 00010100 response, status = self.post(self.known_resource_url, data=data) self.assert201(status) where = '?where={"prog": {"$bitsAllClear": [1, 5]}}' response, status = self.get(self.known_resource, where) self.assert200(status) - items = response['_items'] + items = response["_items"] self.assertEqual(1, len(items)) where = '?where={"prog": {"$bitsAllClear": [2, 5]}}' response, status = self.get(self.known_resource, where) self.assert200(status) - items = response['_items'] + items = response["_items"] self.assertEqual(0, len(items)) def assertGet(self, response, status, resource=None): self.assert200(status) - links = response['_links'] + links = response["_links"] self.assertEqual(len(links), 4) self.assertHomeLink(links) if not resource: @@ -1572,22 +1564,21 @@ def assertGet(self, response, status, resource=None): self.assertResourceLink(links, resource) self.assertNextLink(links, 2) - resource = response['_items'] - self.assertEqual(len(resource), self.app.config['PAGINATION_DEFAULT']) + resource = response["_items"] + self.assertEqual(len(resource), self.app.config["PAGINATION_DEFAULT"]) for item in resource: self.assertItem(item, self.known_resource) - etag = item.get(self.app.config['ETAG']) + etag = item.get(self.app.config["ETAG"]) self.assertTrue(etag is not None) class TestGetItem(TestBase): - def assertItemResponse(self, response, status, resource=None): self.assert200(status) - self.assertTrue(self.app.config['ETAG'] in response) - links = response['_links'] + self.assertTrue(self.app.config["ETAG"] in response) + links = response["_links"] self.assertEqual(len(links), 3) self.assertHomeLink(links) self.assertCollectionLink(links, resource or self.known_resource) @@ -1598,12 +1589,10 @@ def test_disallowed_getitem(self): self.assert404(status) def test_getitem_by_id(self): - response, status = self.get(self.known_resource, - item=self.item_id) + response, status = self.get(self.known_resource, item=self.item_id) self.assertItemResponse(response, status) - response, status = self.get(self.known_resource, - item=self.unknown_item_id) + response, status = self.get(self.known_resource, item=self.unknown_item_id) self.assert404(status) def test_getitem_internal_by_id(self): @@ -1612,38 +1601,30 @@ def test_getitem_internal_by_id(self): self.assert200(status) def test_getitem_noschema(self): - self.app.config['DOMAIN'][self.known_resource]['schema'] = {} + self.app.config["DOMAIN"][self.known_resource]["schema"] = {} response, status = self.get(self.known_resource, item=self.item_id) self.assertItemResponse(response, status) def test_getitem_by_name(self): - response, status = self.get(self.known_resource, - item=self.item_name) + response, status = self.get(self.known_resource, item=self.item_name) self.assertItemResponse(response, status) - response, status = self.get(self.known_resource, - item=self.unknown_item_name) + response, status = self.get(self.known_resource, item=self.unknown_item_name) self.assert404(status) def test_getitem_by_name_self_href(self): - response, status = self.get(self.known_resource, - item=self.item_id) - self_href = response['_links']['self']['href'] + response, status = self.get(self.known_resource, item=self.item_id) + self_href = response["_links"]["self"]["href"] - response, status = self.get(self.known_resource, - item=self.item_name) + response, status = self.get(self.known_resource, item=self.item_name) - self.assertEqual(self_href, response['_links']['self']['href']) + self.assertEqual(self_href, response["_links"]["self"]["href"]) def test_getitem_by_integer(self): - self.domain['contacts']['additional_lookup'] = { - 'field': 'prog' - } - self.app._add_resource_url_rules('contacts', self.domain['contacts']) - response, status = self.get(self.known_resource, - item=1) + self.domain["contacts"]["additional_lookup"] = {"field": "prog"} + self.app._add_resource_url_rules("contacts", self.domain["contacts"]) + response, status = self.get(self.known_resource, item=1) self.assertItemResponse(response, status) - response, status = self.get(self.known_resource, - item=self.known_resource_count) + response, status = self.get(self.known_resource, item=self.known_resource_count) self.assert404(status) def test_getitem_if_modified_since(self): @@ -1651,30 +1632,30 @@ def test_getitem_if_modified_since(self): def test_getitem_if_none_match(self): r = self.test_client.get(self.item_id_url) - etag = r.headers.get('ETag') + etag = r.headers.get("ETag") self.assertTrue(etag is not None) # test that ETag is compliant to RFC 7232-2.3 and #794 is fixed. self.assertTrue(etag[0] == '"') self.assertTrue(etag[-1] == '"') - r = self.test_client.get(self.item_id_url, - headers=[('If-None-Match', etag)]) + r = self.test_client.get(self.item_id_url, headers=[("If-None-Match", etag)]) self.assert304(r.status_code) self.assertTrue(not r.get_data()) # test that we also support doublequote-less etags, for legacy # reasons. See #794. - r = self.test_client.get(self.item_id_url, - headers=[('If-None-Match', - etag.replace('"', ''))]) + r = self.test_client.get( + self.item_id_url, headers=[("If-None-Match", etag.replace('"', ""))] + ) self.assert304(r.status_code) self.assertTrue(not r.get_data()) # test that we support weak etags - weak_etag = 'W/' + etag - r = self.test_client.get(self.item_id_url, - headers=[('If-None-Match', weak_etag)]) + weak_etag = "W/" + etag + r = self.test_client.get( + self.item_id_url, headers=[("If-None-Match", weak_etag)] + ) self.assert304(r.status_code) self.assertTrue(not r.get_data()) @@ -1685,20 +1666,18 @@ def test_expires(self): self.assertExpires(self.item_id_url) def test_getitem_by_id_different_resource(self): - response, status = self.get(self.different_resource, - item=self.user_id) + response, status = self.get(self.different_resource, item=self.user_id) self.assertItemResponse(response, status, self.different_resource) - response, status = self.get(self.different_resource, - item=self.item_id) + response, status = self.get(self.different_resource, item=self.item_id) self.assert404(status) def test_getitem_by_name_different_resource(self): - response, status = self.get(self.different_resource, - item=self.user_username) + response, status = self.get(self.different_resource, item=self.user_username) self.assertItemResponse(response, status, self.different_resource) - response, status = self.get(self.different_resource, - item=self.unknown_item_name) + response, status = self.get( + self.different_resource, item=self.unknown_item_name + ) self.assert404(status) def test_getitem_missing_standard_date_fields(self): @@ -1706,8 +1685,8 @@ def test_getitem_missing_standard_date_fields(self): LAST_UPDATED and/or DATE_CREATED fields. """ contacts = self.random_contacts(1, False) - ref = 'test_update_field' - contacts[0]['ref'] = ref + ref = "test_update_field" + contacts[0]["ref"] = ref _db = self.connection[MONGO_DBNAME] _db.contacts.insert_one(contacts[0]) response, status = self.get(self.known_resource, item=ref) @@ -1715,7 +1694,7 @@ def test_getitem_missing_standard_date_fields(self): def test_get_with_post_override(self): # POST request with GET override turns into a GET - headers = [('X-HTTP-Method-Override', 'GET')] + headers = [("X-HTTP-Method-Override", "GET")] r = self.test_client.post(self.item_id_url, headers=headers) response, status = self.parse_response(r) self.assertItemResponse(response, status) @@ -1726,94 +1705,98 @@ def test_getitem_embedded(self): fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) - invoices = self.domain['invoices'] + invoices = self.domain["invoices"] # Test that we get 400 if can't parse dict - embedded = 'not-a-dict' - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + embedded = "not-a-dict" + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert400(r.status_code) # Test that doesn't come embedded if asking for a field that # isn't embedded (global setting is True by default) embedded = '{"person": 1}' - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue(content['person'], self.item_id) + self.assertTrue(content["person"], self.item_id) # Set field to be embedded - invoices['schema']['person']['data_relation']['embeddable'] = True + invoices["schema"]["person"]["data_relation"]["embeddable"] = True # Test that global setting applies even if field is set to embedded - invoices['embedding'] = False - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + invoices["embedding"] = False + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue(content['person'], self.item_id) + self.assertTrue(content["person"], self.item_id) # Test that it works - invoices['embedding'] = True - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + invoices["embedding"] = True + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['person']) + self.assertTrue("location" in content["person"]) # Test that it ignores a bogus field embedded = '{"person": 1, "not-a-real-field": 1}' - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['person']) + self.assertTrue("location" in content["person"]) # Test that it ignores a real field with a bogus value embedded = '{"person": 1, "inv_number": "not-a-real-value"}' - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['person']) + self.assertTrue("location" in content["person"]) # Test that it works with item endpoint too - r = self.test_client.get('%s/%s/%s' % (invoices['url'], - self.invoice_id, - '?embedded=%s' % embedded)) + r = self.test_client.get( + "%s/%s/%s" % (invoices["url"], self.invoice_id, "?embedded=%s" % embedded) + ) self.assert200(r.status_code) content = json.loads(r.get_data()) - self.assertTrue('location' in content['person']) + self.assertTrue("location" in content["person"]) # Test that changes to embedded document invalidate parent cache - invoice_last_modified = r.headers.get('Last-Modified') - contact_url = '%s/%s' % (self.domain['contacts']['url'], - fake_contact_id) + invoice_last_modified = r.headers.get("Last-Modified") + contact_url = "%s/%s" % (self.domain["contacts"]["url"], fake_contact_id) r = self.test_client.get(contact_url) - contact_etag = r.headers.get('Etag') + contact_etag = r.headers.get("Etag") # wait for contact and invoice updated at diff to pass 1s resolution time.sleep(2) - changes = {'location': {'city': 'new city'}} - response, status = self.patch(contact_url, data=changes, - headers=[('If-Match', contact_etag)]) + changes = {"location": {"city": "new city"}} + response, status = self.patch( + contact_url, data=changes, headers=[("If-Match", contact_etag)] + ) self.assert200(status) - invoice_url = '%s/%s/%s' % (invoices['url'], self.invoice_id, - '?embedded=%s' % embedded) - r = self.test_client.get(invoice_url, - headers=[('If-Modified-Since', - invoice_last_modified)]) + invoice_url = "%s/%s/%s" % ( + invoices["url"], + self.invoice_id, + "?embedded=%s" % embedded, + ) + r = self.test_client.get( + invoice_url, headers=[("If-Modified-Since", invoice_last_modified)] + ) self.assert200(r.status_code) def test_subresource_getitem(self): @@ -1823,27 +1806,29 @@ def test_subresource_getitem(self): fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices/%s' % (fake_contact_id, - self.invoice_id)) + response, status = self.get( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id) + ) self.assert200(status) - self.assertEqual(response['person'], str(fake_contact_id)) - self.assertEqual(response['_id'], self.invoice_id) + self.assertEqual(response["person"], str(fake_contact_id)) + self.assertEqual(response["_id"], self.invoice_id) def test_getitem_ifmatch_disabled(self): # when IF_MATCH is disabled no etag is present in payload - self.app.config['IF_MATCH'] = False + self.app.config["IF_MATCH"] = False response, _ = self.get(self.known_resource, item=self.item_id) - self.assertTrue(self.app.config['ETAG'] not in response) + self.assertTrue(self.app.config["ETAG"] not in response) def test_getitem_ifmatch_disabled_if_mod_since(self): # Test that #239 is fixed. # IF_MATCH is disabled and If-Modified-Since request comes through. If # a 304 was expected, we would crash like a mofo. - self.app.config['IF_MATCH'] = False + self.app.config["IF_MATCH"] = False # IMS needs to see as recent as possible since the test db has just # been built @@ -1853,65 +1838,66 @@ def test_getitem_ifmatch_disabled_if_mod_since(self): self.assert304(r.status_code) def test_getitem_custom_auto_document_fields(self): - self.app.config['LAST_UPDATED'] = '_updated_on' - self.app.config['DATE_CREATED'] = '_created_on' - self.app.config['ETAG'] = '_the_etag' + self.app.config["LAST_UPDATED"] = "_updated_on" + self.app.config["DATE_CREATED"] = "_created_on" + self.app.config["ETAG"] = "_the_etag" response, _ = self.get(self.known_resource, item=self.item_id) - self.assertTrue('_updated_on' in response) - self.assertTrue('_created_on' in response) - self.assertTrue('_the_etag' in response) + self.assertTrue("_updated_on" in response) + self.assertTrue("_created_on" in response) + self.assertTrue("_the_etag" in response) def test_getitem_projection(self): projection = '{"prog": 1}' - r, status = self.get(self.known_resource, '?projection=%s' % - projection, item=self.item_id) - self.assert200(status) - self.assertFalse('location' in r) - self.assertFalse('role' in r) - self.assertTrue('prog' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + r, status = self.get( + self.known_resource, "?projection=%s" % projection, item=self.item_id + ) + self.assert200(status) + self.assertFalse("location" in r) + self.assertFalse("role" in r) + self.assertTrue("prog" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) projection = '{"prog": 0}' - r, status = self.get(self.known_resource, '?projection=%s' % - projection, item=self.item_id) - self.assert200(status) - self.assertFalse('prog' in r) - self.assertTrue('location' in r) - self.assertTrue('role' in r) - self.assertTrue(self.domain[self.known_resource]['id_field'] in r) - self.assertTrue(self.app.config['ETAG'] in r) - self.assertTrue(self.app.config['LAST_UPDATED'] in r) - self.assertTrue(self.app.config['DATE_CREATED'] in r) - self.assertTrue(r[self.app.config['LAST_UPDATED']] != self.epoch) - self.assertTrue(r[self.app.config['DATE_CREATED']] != self.epoch) + r, status = self.get( + self.known_resource, "?projection=%s" % projection, item=self.item_id + ) + self.assert200(status) + self.assertFalse("prog" in r) + self.assertTrue("location" in r) + self.assertTrue("role" in r) + self.assertTrue(self.domain[self.known_resource]["id_field"] in r) + self.assertTrue(self.app.config["ETAG"] in r) + self.assertTrue(self.app.config["LAST_UPDATED"] in r) + self.assertTrue(self.app.config["DATE_CREATED"] in r) + self.assertTrue(r[self.app.config["LAST_UPDATED"]] != self.epoch) + self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_getitem_lookup_field_as_string(self): # Test that a resource where 'item_lookup_field' is set to a field # of string type and which value is castable to a ObjectId is still # treated as a string when 'query_objectid_as_string' is set to True. # See PR #552. - data = {'id': '507c7f79bcf86cd7994f6c0e', 'name': 'john'} - response, status = self.post('ids', data=data) + data = {"id": "507c7f79bcf86cd7994f6c0e", "name": "john"} + response, status = self.post("ids", data=data) self.assert201(status) - response, status = self.get('ids', item='507c7f79bcf86cd7994f6c0e') + response, status = self.get("ids", item="507c7f79bcf86cd7994f6c0e") self.assert200(status) def test_getitem_with_custom_idfield(self): _db = self.connection[MONGO_DBNAME] - sku = _db.products.find()[0]['sku'] - response, status = self.get('products', item=sku) - self.assertItemResponse(response, status, 'products') + sku = _db.products.find()[0]["sku"] + response, status = self.get("products", item=sku) + self.assertItemResponse(response, status, "products") class TestHead(TestBase): - def test_head_home(self): - self.assertHead('/') + self.assertHead("/") def test_head_resource(self): self.assertHead(self.known_resource_url) @@ -1924,11 +1910,11 @@ def assertHead(self, url): r = self.test_client.get(url) self.assertTrue(not h.data) - if 'Expires' in r.headers: + if "Expires" in r.headers: # there's a tiny chance that the two expire values will differ by # one second. See #316. - head_expire = str_to_date(r.headers.pop('Expires')) - get_expire = str_to_date(h.headers.pop('Expires')) + head_expire = str_to_date(r.headers.pop("Expires")) + get_expire = str_to_date(h.headers.pop("Expires")) d = head_expire - get_expire self.assertTrue(d.seconds in (0, 1)) @@ -1936,7 +1922,6 @@ def assertHead(self, url): class TestEvents(TestBase): - def setUp(self): super(TestEvents, self).setUp() self.devent = DummyEvent(lambda: True) @@ -1944,18 +1929,18 @@ def setUp(self): def test_on_pre_GET_for_item(self): self.app.on_pre_GET += self.devent self.get_item() - self.assertEqual('contacts', self.devent.called[0]) + self.assertEqual("contacts", self.devent.called[0]) self.assertFalse(self.devent.called[1] is None) def test_on_pre_GET_item_dynamic_filter(self): def filter_this(resource, request, lookup): lookup["_id"] = self.item_id + self.app.on_pre_GET += filter_this # Would normally return a 404; will return one instead. r, s = self.parse_response(self.get_item()) self.assert200(s) - self.assertEqual(r[self.domain[self.known_resource]['id_field']], - self.item_id) + self.assertEqual(r[self.domain[self.known_resource]["id_field"]], self.item_id) def test_on_pre_GET_resource_for_item(self): self.app.on_pre_GET_contacts += self.devent @@ -1970,28 +1955,25 @@ def test_on_pre_GET_for_resource(self): def test_on_pre_GET_resource_dynamic_filter(self): def filter_this(resource, request, lookup): lookup["_id"] = self.item_id + self.app.on_pre_GET += filter_this # Would normally return all documents; will only just one. r, s = self.parse_response(self.get_resource()) - self.assertEqual(len(r[self.app.config['ITEMS']]), 1) + self.assertEqual(len(r[self.app.config["ITEMS"]]), 1) def test_on_pre_GET_resource_dynamic_filter_12_chr_nonunicode_string(self): # Test for bug in _mongotize(). See # https://github.com/nicolaiarocci/eve/issues/508 def filter_this(request, lookup): - request.args = ImmutableMultiDict( - {"where": '{"name":"Alice Brooks"}'} - ) - self.app.register_resource( - 'names', - {'schema': {'name': {'type': 'string'}}} - ) + request.args = ImmutableMultiDict({"where": '{"name":"Alice Brooks"}'}) + + self.app.register_resource("names", {"schema": {"name": {"type": "string"}}}) # We want to test with a non-unicode string for 'where', so we need to # do it with a pre_GET callback self.app.on_pre_GET_names += filter_this - self.post('names', data={"name": "Alice Brooks"}) - r, s = self.get('names') - self.assertEqual(len(r[self.app.config['ITEMS']]), 1) + self.post("names", data={"name": "Alice Brooks"}) + r, s = self.get("names") + self.assertEqual(len(r[self.app.config["ITEMS"]]), 1) def test_on_pre_GET_resource_for_resource(self): self.app.on_pre_GET_contacts += self.devent @@ -2020,37 +2002,39 @@ def test_on_post_GET_resource_for_resource(self): def test_on_post_GET_homepage(self): self.app.on_post_GET += self.devent - self.test_client.get('/') + self.test_client.get("/") self.assertTrue(self.devent.called[0] is None) self.assertEqual(3, len(self.devent.called)) def test_on_fetched_resource(self): self.app.on_fetched_resource += self.devent self.get_resource() - self.assertEqual('contacts', self.devent.called[0]) + self.assertEqual("contacts", self.devent.called[0]) self.assertEqual( - self.app.config['PAGINATION_DEFAULT'], - len(self.devent.called[1][self.app.config['ITEMS']])) + self.app.config["PAGINATION_DEFAULT"], + len(self.devent.called[1][self.app.config["ITEMS"]]), + ) def test_on_fetched_resource_contacts(self): self.app.on_fetched_resource_contacts += self.devent self.get_resource() self.assertEqual( - self.app.config['PAGINATION_DEFAULT'], - len(self.devent.called[0][self.app.config['ITEMS']])) + self.app.config["PAGINATION_DEFAULT"], + len(self.devent.called[0][self.app.config["ITEMS"]]), + ) def test_on_fetched_item(self): self.app.on_fetched_item += self.devent self.get_item() - self.assertEqual('contacts', self.devent.called[0]) - id_field = self.domain[self.known_resource]['id_field'] + self.assertEqual("contacts", self.devent.called[0]) + id_field = self.domain[self.known_resource]["id_field"] self.assertEqual(self.item_id, str(self.devent.called[1][id_field])) self.assertEqual(2, len(self.devent.called)) def test_on_fetched_item_contacts(self): self.app.on_fetched_item_contacts += self.devent self.get_item() - id_field = self.domain[self.known_resource]['id_field'] + id_field = self.domain[self.known_resource]["id_field"] self.assertEqual(self.item_id, str(self.devent.called[0][id_field])) self.assertEqual(1, len(self.devent.called)) @@ -2061,33 +2045,33 @@ def test_get_before_aggregation_hook(self): {"x": 1, "tags": ["dog", "cat"]}, {"x": 2, "tags": ["cat"]}, {"x": 2, "tags": ["mouse", "cat", "dog"]}, - {"x": 3, "tags": []} + {"x": 3, "tags": []}, ] ) self.app.before_aggregation += self.devent self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [ {"$unwind": "$tags"}, - {"$group": {"_id": "$tags", "count": {"$sum": - "$field1"}}}, - ], + {"$group": {"_id": "$tags", "count": {"$sum": "$field1"}}}, + ] } } - } + }, ) - response, status = self.get('aggregate_test?aggregate=ciao') + response, status = self.get("aggregate_test?aggregate=ciao") self.assert400(status) self.assertTrue(self.devent.called is None) response, status = self.get('aggregate_test?aggregate={"$field1":1}') self.assert200(status) - self.assertEqual('aggregate_test', self.devent.called[0]) + self.assertEqual("aggregate_test", self.devent.called[0]) def test_get_after_aggregation_hook(self): _db = self.connection[MONGO_DBNAME] @@ -2096,33 +2080,33 @@ def test_get_after_aggregation_hook(self): {"x": 1, "tags": ["dog", "cat"]}, {"x": 2, "tags": ["cat"]}, {"x": 2, "tags": ["mouse", "cat", "dog"]}, - {"x": 3, "tags": []} + {"x": 3, "tags": []}, ] ) self.app.after_aggregation += self.devent self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [ {"$unwind": "$tags"}, - {"$group": {"_id": "$tags", "count": {"$sum": - "$field1"}}}, - ], + {"$group": {"_id": "$tags", "count": {"$sum": "$field1"}}}, + ] } } - } + }, ) - response, status = self.get('aggregate_test?aggregate=ciao') + response, status = self.get("aggregate_test?aggregate=ciao") self.assert400(status) self.assertTrue(self.devent.called is None) response, status = self.get('aggregate_test?aggregate={"$field1":1}') self.assert200(status) - self.assertEqual('aggregate_test', self.devent.called[0]) + self.assertEqual("aggregate_test", self.devent.called[0]) def get_resource(self): return self.test_client.get(self.known_resource_url) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index a4f545db8..638b71568 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -13,7 +13,6 @@ class TestPatch(TestBase): - def test_patch_to_resource_endpoint(self): _, status = self.patch(self.known_resource_url, data={}) self.assert405(status) @@ -23,62 +22,66 @@ def test_readonly_resource(self): self.assert405(status) def test_unknown_id(self): - _, status = self.patch(self.unknown_item_id_url, - data={"key1": 'value1'}) + _, status = self.patch(self.unknown_item_id_url, data={"key1": "value1"}) self.assert404(status) def test_unknown_id_different_resource(self): # patching a 'user' with a valid 'contact' id will 404 - _, status = self.patch('%s/%s/' % (self.different_resource, - self.item_id), - data={"key1": "value1"}) + _, status = self.patch( + "%s/%s/" % (self.different_resource, self.item_id), data={"key1": "value1"} + ) self.assert404(status) # of course we can still patch a 'user' - _, status = self.patch('%s/%s/' % (self.different_resource, - self.user_id), - data={'key1': '{"username": "username1"}'}, - headers=[('If-Match', self.user_etag)]) + _, status = self.patch( + "%s/%s/" % (self.different_resource, self.user_id), + data={"key1": '{"username": "username1"}'}, + headers=[("If-Match", self.user_etag)], + ) self.assert200(status) def test_by_name(self): - _, status = self.patch(self.item_name_url, data={'key1': 'value1'}) + _, status = self.patch(self.item_name_url, data={"key1": "value1"}) self.assert405(status) def test_ifmatch_missing(self): - res, status = self.patch(self.item_id_url, data={'key1': 'value1'}) + res, status = self.patch(self.item_id_url, data={"key1": "value1"}) self.assert428(status) def test_ifmatch_missing_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - r, status = self.patch(self.item_id_url, data={'key1': 'value1'}) + self.app.config["ENFORCE_IF_MATCH"] = False + r, status = self.patch(self.item_id_url, data={"key1": "value1"}) self.assert200(status) self.assertTrue(ETAG in r) def test_ifmatch_disabled(self): - self.app.config['IF_MATCH'] = False - r, status = self.patch(self.item_id_url, data={'key1': 'value1'}) + self.app.config["IF_MATCH"] = False + r, status = self.patch(self.item_id_url, data={"key1": "value1"}) self.assert200(status) self.assertTrue(ETAG not in r) def test_ifmatch_disabled_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - self.app.config['IF_MATCH'] = False - r, status = self.patch(self.item_id_url, data={'key1': 'value1'}) + self.app.config["ENFORCE_IF_MATCH"] = False + self.app.config["IF_MATCH"] = False + r, status = self.patch(self.item_id_url, data={"key1": "value1"}) self.assert200(status) self.assertTrue(ETAG not in r) def test_ifmatch_bad_etag(self): - _, status = self.patch(self.item_id_url, - data={'key1': 'value1'}, - headers=[('If-Match', 'not-quite-right')]) + _, status = self.patch( + self.item_id_url, + data={"key1": "value1"}, + headers=[("If-Match", "not-quite-right")], + ) self.assert412(status) def test_ifmatch_bad_etag_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - _, status = self.patch(self.item_id_url, - data={'key1': 'value1'}, - headers=[('If-Match', 'not-quite-right')]) + self.app.config["ENFORCE_IF_MATCH"] = False + _, status = self.patch( + self.item_id_url, + data={"key1": "value1"}, + headers=[("If-Match", "not-quite-right")], + ) self.assert412(status) def test_unique_value(self): @@ -88,12 +91,15 @@ def test_unique_value(self): # unit tests. This test also makes sure that response status is # syntactically correct in case of validation issues. # We should probably test every single case as well (seems overkill). - r, status = self.patch(self.item_id_url, - data={"ref": "%s" % self.alt_ref}, - headers=[('If-Match', self.item_etag)]) + r, status = self.patch( + self.item_id_url, + data={"ref": "%s" % self.alt_ref}, + headers=[("If-Match", self.item_etag)], + ) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'ref': "value '%s' is not unique" % - self.alt_ref}) + self.assertValidationError( + r, {"ref": "value '%s' is not unique" % self.alt_ref} + ) def test_patch_string(self): field = "ref" @@ -121,10 +127,7 @@ def test_patch_list_as_array(self): def test_patch_rows(self): field = "rows" - test_value = [ - {'sku': 'AT1234', 'price': 99}, - {'sku': 'XF9876', 'price': 9999} - ] + test_value = [{"sku": "AT1234", "price": 99}, {"sku": "XF9876", "price": 9999}] changes = {field: test_value} r = self.perform_patch(changes) db_value = self.compare_patch_with_get(field, r) @@ -142,12 +145,12 @@ def test_patch_list(self): def test_patch_dict(self): field = "location" - test_value = {'address': 'an address', 'city': 'a city'} + test_value = {"address": "an address", "city": "a city"} changes = {field: test_value} original_city = [] def keep_original_city(resource_name, updates, original): - original_city.append(original['location']['city']) + original_city.append(original["location"]["city"]) self.app.on_update += keep_original_city self.app.on_updated += keep_original_city @@ -190,7 +193,7 @@ def test_patch_missing_default(self): test_value = "1234567890123456789012345" changes = {field: test_value} r = self.perform_patch(changes) - self.assertEqual(self.compare_patch_with_get('title', r), 'Mr.') + self.assertEqual(self.compare_patch_with_get("title", r), "Mr.") def test_patch_missing_default_with_post_override(self): """ PATCH an object which is missing a field with a default value. @@ -201,14 +204,17 @@ def test_patch_missing_default_with_post_override(self): test_value = "1234567890123456789012345" r = self.perform_patch_with_post_override(field, test_value) self.assert200(r.status_code) - title = self.compare_patch_with_get('title', json.loads(r.get_data())) - self.assertEqual(title, 'Mr.') + title = self.compare_patch_with_get("title", json.loads(r.get_data())) + self.assertEqual(title, "Mr.") def test_patch_multiple_fields(self): - fields = ['ref', 'prog', 'role'] + fields = ["ref", "prog", "role"] test_values = ["9876543210987654321054321", 123, ["agent"]] - changes = {"ref": test_values[0], "prog": test_values[1], - "role": test_values[2]} + changes = { + "ref": test_values[0], + "prog": test_values[1], + "role": test_values[2], + } r = self.perform_patch(changes) db_values = self.compare_patch_with_get(fields, r) for i in range(len(db_values)): @@ -216,18 +222,21 @@ def test_patch_multiple_fields(self): def test_patch_with_post_override(self): # a POST request with PATCH override turns into a PATCH request - r = self.perform_patch_with_post_override('prog', 1) + r = self.perform_patch_with_post_override("prog", 1) self.assert200(r.status_code) def test_patch_internal(self): # test that patch_internal is available and working properly. - test_field = 'ref' + test_field = "ref" test_value = "9876543210987654321098765" data = {test_field: test_value} with self.app.test_request_context(self.item_id_url): r, _, _, status = patch_internal( - self.known_resource, data, concurrency_check=False, - **{'_id': self.item_id}) + self.known_resource, + data, + concurrency_check=False, + **{"_id": self.item_id} + ) db_value = self.compare_patch_with_get(test_field, r) self.assertEqual(db_value, test_value) self.assert200(status) @@ -235,63 +244,64 @@ def test_patch_internal(self): def test_patch_etag_header(self): # test that Etag is always included with response header. See #562. changes = {"ref": "1234567890123456789012345"} - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] - r = self.test_client.patch(self.item_id_url, - data=json.dumps(changes), - headers=headers) - self.assertTrue('Etag' in r.headers) + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] + r = self.test_client.patch( + self.item_id_url, data=json.dumps(changes), headers=headers + ) + self.assertTrue("Etag" in r.headers) # test that ETag is compliant to RFC 7232-2.3 and #794 is fixed. - etag = r.headers['ETag'] + etag = r.headers["ETag"] self.assertTrue(etag[0] == '"') self.assertTrue(etag[-1] == '"') def test_patch_etag_header_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - changes = {'ref': '1234567890123456789012345'} - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] + self.app.config["ENFORCE_IF_MATCH"] = False + changes = {"ref": "1234567890123456789012345"} + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] r, status = self.patch( - self.item_id_url, - data=json.dumps(changes), - headers=headers + self.item_id_url, data=json.dumps(changes), headers=headers ) self.assertTrue(ETAG in r) self.assertTrue(self.item_etag != r[ETAG]) def test_patch_nested(self): - changes = {'location.city': 'a nested city', - 'location.address': 'a nested address'} + changes = { + "location.city": "a nested city", + "location.address": "a nested address", + } r = self.perform_patch(changes) - values = self.compare_patch_with_get('location', r) - self.assertEqual(values['city'], 'a nested city') - self.assertEqual(values['address'], 'a nested address') + values = self.compare_patch_with_get("location", r) + self.assertEqual(values["city"], "a nested city") + self.assertEqual(values["address"], "a nested address") def perform_patch(self, changes): - r, status = self.patch(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) self.assertPatchResponse(r, self.item_id) return r def perform_patch_with_post_override(self, field, value): - headers = [('X-HTTP-Method-Override', 'PATCH'), - ('If-Match', self.item_etag), - ('Content-Type', 'application/json')] - return self.test_client.post(self.item_id_url, - data=json.dumps({field: value}), - headers=headers) + headers = [ + ("X-HTTP-Method-Override", "PATCH"), + ("If-Match", self.item_etag), + ("Content-Type", "application/json"), + ] + return self.test_client.post( + self.item_id_url, data=json.dumps({field: value}), headers=headers + ) def compare_patch_with_get(self, fields, patch_response): raw_r = self.test_client.get(self.item_id_url) r, status = self.parse_response(raw_r) self.assert200(status) - self.assertEqual(raw_r.headers.get('ETag').replace('"', ''), - patch_response[ETAG]) + self.assertEqual( + raw_r.headers.get("ETag").replace('"', ""), patch_response[ETAG] + ) if isinstance(fields, str): return r[fields] else: @@ -299,15 +309,15 @@ def compare_patch_with_get(self, fields, patch_response): def test_patch_allow_unknown(self): changes = {"unknown": "unknown"} - r, status = self.patch(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'unknown': 'unknown field'}) - self.app.config['DOMAIN'][self.known_resource]['allow_unknown'] = True - r, status = self.patch(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + self.assertValidationError(r, {"unknown": "unknown field"}) + self.app.config["DOMAIN"][self.known_resource]["allow_unknown"] = True + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) self.assertPatchResponse(r, self.item_id) @@ -315,32 +325,36 @@ def test_patch_x_www_form_urlencoded(self): field = "ref" test_value = "1234567890123456789012345" changes = {field: test_value} - headers = [('If-Match', self.item_etag)] - r, status = self.parse_response(self.test_client.patch( - self.item_id_url, data=changes, headers=headers)) + headers = [("If-Match", self.item_etag)] + r, status = self.parse_response( + self.test_client.patch(self.item_id_url, data=changes, headers=headers) + ) self.assert200(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) def test_patch_x_www_form_urlencoded_number_serialization(self): - del(self.domain['contacts']['schema']['ref']['required']) - field = 'anumber' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + field = "anumber" test_value = 3.5 changes = {field: test_value} - headers = [('If-Match', self.item_etag)] - r, status = self.parse_response(self.test_client.patch( - self.item_id_url, data=changes, headers=headers)) + headers = [("If-Match", self.item_etag)] + r, status = self.parse_response( + self.test_client.patch(self.item_id_url, data=changes, headers=headers) + ) self.assert200(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) def test_patch_referential_integrity(self): data = {"person": self.unknown_item_id} - headers = [('If-Match', self.invoice_etag)] + headers = [("If-Match", self.invoice_etag)] r, status = self.patch(self.invoice_id_url, data=data, headers=headers) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) - self.assertValidationError(r, {'person': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) + self.assertValidationError(r, {"person": expected}) data = {"person": self.item_id} r, status = self.patch(self.invoice_id_url, data=data, headers=headers) @@ -350,24 +364,24 @@ def test_patch_referential_integrity(self): def test_patch_write_concern_success(self): # 0 and 1 are the only valid values for 'w' on our mongod instance (1 # is the default) - self.domain['contacts']['mongo_write_concern'] = {'w': 0} + self.domain["contacts"]["mongo_write_concern"] = {"w": 0} field = "ref" test_value = "X234567890123456789012345" changes = {field: test_value} - _, status = self.patch(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + _, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) def test_patch_write_concern_fail(self): # should get a 500 since there's no replicaset on the mongod instance - self.domain['contacts']['mongo_write_concern'] = {'w': 2} + self.domain["contacts"]["mongo_write_concern"] = {"w": 2} field = "ref" test_value = "X234567890123456789012345" changes = {field: test_value} - _, status = self.patch(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + _, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert500(status) def test_patch_missing_standard_date_fields(self): @@ -377,8 +391,8 @@ def test_patch_missing_standard_date_fields(self): # directly insert a document, without DATE_CREATED e LAST_UPDATED # values. contacts = self.random_contacts(1, False) - ref = 'test_update_field' - contacts[0]['ref'] = ref + ref = "test_update_field" + contacts[0]["ref"] = ref _db = self.connection[MONGO_DBNAME] _db.contacts.insert_one(contacts[0]) @@ -387,14 +401,17 @@ def test_patch_missing_standard_date_fields(self): # values. response, status = self.get(self.known_resource, item=ref) etag = response[ETAG] - _id = response['_id'] + _id = response["_id"] # attempt a PATCH with the new etag. field = "ref" test_value = "X234567890123456789012345" changes = {field: test_value} - _, status = self.patch('%s/%s' % (self.known_resource_url, _id), - data=changes, headers=[('If-Match', etag)]) + _, status = self.patch( + "%s/%s" % (self.known_resource_url, _id), + data=changes, + headers=[("If-Match", etag)], + ) self.assert200(status) def test_patch_subresource(self): @@ -405,70 +422,76 @@ def test_patch_subresource(self): fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id)) + response, status = self.get( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id) + ) etag = response[ETAG] data = {"inv_number": "new_number"} - headers = [('If-Match', etag)] - response, status = self.patch('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id), - data=data, headers=headers) + headers = [("If-Match", etag)] + response, status = self.patch( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id), + data=data, + headers=headers, + ) self.assert200(status) - self.assertPatchResponse(response, self.invoice_id, 'peopleinvoices') + self.assertPatchResponse(response, self.invoice_id, "peopleinvoices") def test_patch_bandwidth_saver(self): - changes = {'ref': '1234567890123456789012345'} + changes = {"ref": "1234567890123456789012345"} # bandwidth_saver is on by default - self.assertTrue(self.app.config['BANDWIDTH_SAVER']) + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) r = self.perform_patch(changes) - self.assertFalse('ref' in r) - db_value = self.compare_patch_with_get(self.app.config['ETAG'], r) - self.assertEqual(db_value, r[self.app.config['ETAG']]) - self.item_etag = r[self.app.config['ETAG']] + self.assertFalse("ref" in r) + db_value = self.compare_patch_with_get(self.app.config["ETAG"], r) + self.assertEqual(db_value, r[self.app.config["ETAG"]]) + self.item_etag = r[self.app.config["ETAG"]] # test return all fields (bandwidth_saver off) - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False r = self.perform_patch(changes) - self.assertTrue('ref' in r) - db_value = self.compare_patch_with_get(self.app.config['ETAG'], r) - self.assertEqual(db_value, r[self.app.config['ETAG']]) + self.assertTrue("ref" in r) + db_value = self.compare_patch_with_get(self.app.config["ETAG"], r) + self.assertEqual(db_value, r[self.app.config["ETAG"]]) def test_patch_readonly_field_with_previous_document(self): - schema = self.domain['contacts']['schema'] - del(schema['ref']['required']) + schema = self.domain["contacts"]["schema"] + del (schema["ref"]["required"]) # disable read-only on the field so we can store a value which is # also different form its default value. - schema['read_only_field']['readonly'] = False - changes = {'read_only_field': 'value'} + schema["read_only_field"]["readonly"] = False + changes = {"read_only_field": "value"} r = self.perform_patch(changes) # resume read-only status for the field - self.domain['contacts']['schema']['read_only_field']['readonly'] = True + self.domain["contacts"]["schema"]["read_only_field"]["readonly"] = True # test that if the read-only field is included with the payload and its # value is equal to the one stored with the document, validation # succeeds (#479). - etag = r['_etag'] - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', etag)]) + etag = r["_etag"] + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", etag)] + ) self.assert200(status) self.assertPatchResponse(r, self.item_id) # test that if the read-only field is included with the payload and its # value is different from the stored document, validation fails. - etag = r['_etag'] - changes = {'read_only_field': 'another value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', etag)]) + etag = r["_etag"] + changes = {"read_only_field": "another value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", etag)] + ) self.assert422(status) - self.assertTrue('is read-only' in r['_issues']['read_only_field']) + self.assertTrue("is read-only" in r["_issues"]["read_only_field"]) def test_patch_nested_document_not_overwritten(self): """ Test that nested documents are not overwritten on PATCH and #519 @@ -476,7 +499,7 @@ def test_patch_nested_document_not_overwritten(self): """ schema = { - 'sensor': { + "sensor": { "type": "dict", "schema": { "name": {"type": "string"}, @@ -484,151 +507,108 @@ def test_patch_nested_document_not_overwritten(self): "lat": {"type": "float"}, "value": {"type": "float", "default": 10.3}, "dict": { - 'type': 'dict', - 'schema': { - 'string': {'type': 'string'}, - 'int': {'type': 'integer'}, - } - } - } + "type": "dict", + "schema": { + "string": {"type": "string"}, + "int": {"type": "integer"}, + }, + }, + }, }, - 'test': { - 'type': 'string', - 'readonly': True, - 'default': 'default' - } + "test": {"type": "string", "readonly": True, "default": "default"}, } - self.app.config['BANDWIDTH_SAVER'] = False - self.app.register_resource('sensors', {'schema': schema}) + self.app.config["BANDWIDTH_SAVER"] = False + self.app.register_resource("sensors", {"schema": schema}) changes = { - 'sensor': { - 'name': 'device_name', - 'lon': 43.4, - 'lat': 1.31, - 'dict': {'int': 99} + "sensor": { + "name": "device_name", + "lon": 43.4, + "lat": 1.31, + "dict": {"int": 99}, } } r, status = self.post("sensors", data=changes) self.assert201(status) id, etag, value, test, int = ( - r[self.domain['sensors']['id_field']], + r[self.domain["sensors"]["id_field"]], r[ETAG], - r['sensor']['value'], - r['test'], - r['sensor']['dict']['int'] + r["sensor"]["value"], + r["test"], + r["sensor"]["dict"]["int"], ) - changes = { - 'sensor': { - 'lon': 10.0, - 'dict': {'string': 'hi'} - } - } + changes = {"sensor": {"lon": 10.0, "dict": {"string": "hi"}}} r, status = self.patch( - "/%s/%s" % ('sensors', id), - data=changes, - headers=[('If-Match', etag)] + "/%s/%s" % ("sensors", id), data=changes, headers=[("If-Match", etag)] ) self.assert200(status) - etag, value, int = ( - r[ETAG], - r['sensor']['value'], - r['sensor']['dict']['int'] - ) + etag, value, int = (r[ETAG], r["sensor"]["value"], r["sensor"]["dict"]["int"]) self.assertEqual(value, 10.3) - self.assertEqual(test, 'default') + self.assertEqual(test, "default") self.assertEqual(int, 99) def test_patch_nested_document_no_merge(self): """ Test that nested documents are not merged, but overwritten, if configured.""" domain = { - 'merge_nested_documents': False, - 'schema': { - 'nested': { - 'type': 'dict', - } - } + "merge_nested_documents": False, + "schema": {"nested": {"type": "dict"}}, } - self.app.config['BANDWIDTH_SAVER'] = False - self.app.register_resource('nomerge', domain) + self.app.config["BANDWIDTH_SAVER"] = False + self.app.register_resource("nomerge", domain) - original = { - 'nested': { - 'key1': 'value1', - 'key2': 'value2', - } - } - changes = { - 'nested': { - 'key2': 'value2', - 'key3': 'value3', - } - } + original = {"nested": {"key1": "value1", "key2": "value2"}} + changes = {"nested": {"key2": "value2", "key3": "value3"}} r, status = self.post("nomerge", data=original) self.assert201(status) - id = r['_id'] - etag = r['_etag'] + id = r["_id"] + etag = r["_etag"] r, status = self.patch( - "/%s/%s" % ('nomerge', id), - data=changes, - headers=[('If-Match', etag)] + "/%s/%s" % ("nomerge", id), data=changes, headers=[("If-Match", etag)] ) self.assert200(status) # Assert that nested document was completely overwritten - self.assertEqual(r['nested'], changes['nested']) + self.assertEqual(r["nested"], changes["nested"]) def test_patch_nested_document_nullable_missing(self): schema = { - 'sensor': { - 'type': 'dict', - 'schema': { - 'name': {'type': 'string'}, - }, - 'default': None, - 'nullable': True + "sensor": { + "type": "dict", + "schema": {"name": {"type": "string"}}, + "default": None, + "nullable": True, }, - 'other': { - 'type': 'dict', - 'schema': { - 'name': {'type': 'string'}, - }, - } + "other": {"type": "dict", "schema": {"name": {"type": "string"}}}, } - self.app.config['BANDWIDTH_SAVER'] = False - self.app.register_resource('sensors', {'schema': schema}) + self.app.config["BANDWIDTH_SAVER"] = False + self.app.register_resource("sensors", {"schema": schema}) changes = {} r, status = self.post("sensors", data=changes) self.assert201(status) - id, etag = r[self.domain['sensors']['id_field']], r[ETAG] - self.assertTrue('sensor' in r) - self.assertEqual(r['sensor'], None) - self.assertFalse('other' in r) + id, etag = r[self.domain["sensors"]["id_field"]], r[ETAG] + self.assertTrue("sensor" in r) + self.assertEqual(r["sensor"], None) + self.assertFalse("other" in r) - changes = { - 'sensor': {'name': 'device_name'}, - 'other': {'name': 'other_name'}, - } + changes = {"sensor": {"name": "device_name"}, "other": {"name": "other_name"}} r, status = self.patch( - "/%s/%s" % ('sensors', id), - data=changes, - headers=[('If-Match', etag)] + "/%s/%s" % ("sensors", id), data=changes, headers=[("If-Match", etag)] ) self.assert200(status) - self.assertEqual(r['sensor'], {'name': 'device_name'}) - self.assertEqual(r['other'], {'name': 'other_name'}) + self.assertEqual(r["sensor"], {"name": "device_name"}) + self.assertEqual(r["other"], {"name": "other_name"}) def test_patch_dependent_field_on_origin_document(self): """ Test that when patching a field which is dependent on another and @@ -637,24 +617,27 @@ def test_patch_dependent_field_on_origin_document(self): """ # this will fail as dependent field is missing even in the # document we are trying to update. - del(self.domain['contacts']['schema']['dependency_field1']['default']) - changes = {'dependency_field2': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + del (self.domain["contacts"]["schema"]["dependency_field1"]["default"]) + changes = {"dependency_field2": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert422(status) # update the stored document by adding dependency field. - changes = {'dependency_field1': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + changes = {"dependency_field1": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) # now the field2 update will be accepted as the dependency field is # present in the stored document already. - etag = r['_etag'] - changes = {'dependency_field2': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', etag)]) + etag = r["_etag"] + changes = {"dependency_field2": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", etag)] + ) self.assert200(status) def test_patch_dependent_field_value_on_origin_document(self): @@ -664,57 +647,62 @@ def test_patch_dependent_field_value_on_origin_document(self): """ # this will fail as dependent field is missing even in the # document we are trying to update. - changes = {'dependency_field3': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + changes = {"dependency_field3": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert422(status) # update the stored document by setting the dependency field to # the required value. - changes = {'dependency_field1': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + changes = {"dependency_field1": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) # now the field2 update will be accepted as the dependency field is # present in the stored document already. - etag = r['_etag'] - changes = {'dependency_field3': 'value'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', etag)]) + etag = r["_etag"] + changes = {"dependency_field3": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", etag)] + ) self.assert200(status) def test_id_field_in_document_fails(self): # since v0.6 we also allow the id field to be included with the POSTed # document, but not with PATCH since it is immutable - self.app.config['IF_MATCH'] = False - id_field = self.domain[self.known_resource]['id_field'] - data = {id_field: '55b2340538345bd048100ffe'} + self.app.config["IF_MATCH"] = False + id_field = self.domain[self.known_resource]["id_field"] + data = {id_field: "55b2340538345bd048100ffe"} r, status = self.patch(self.item_id_url, data=data) self.assert400(status) - self.assertTrue('immutable' in r['_error']['message']) + self.assertTrue("immutable" in r["_error"]["message"]) def test_patch_custom_idfield(self): - response, status = self.get('products?max_results=1') - product = response['_items'][0] - headers = [('If-Match', product[ETAG])] - data = {'title': 'Awesome product'} - r, status = self.patch('products/%s' % product['sku'], data=data, - headers=headers) + response, status = self.get("products?max_results=1") + product = response["_items"][0] + headers = [("If-Match", product[ETAG])] + data = {"title": "Awesome product"} + r, status = self.patch( + "products/%s" % product["sku"], data=data, headers=headers + ) self.assert200(status) def test_patch_type_coercion(self): - schema = self.domain[self.known_resource]['schema'] - schema['aninteger']['coerce'] = lambda string: int(float(string)) - changes = {'ref': '1234567890123456789054321', 'aninteger': '42.3'} - r, status = self.patch(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + schema = self.domain[self.known_resource]["schema"] + schema["aninteger"]["coerce"] = lambda string: int(float(string)) + changes = {"ref": "1234567890123456789054321", "aninteger": "42.3"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) - r, status = self.get(r['_links']['self']['href']) - self.assertEqual(r['aninteger'], 42) + r, status = self.get(r["_links"]["self"]["href"]) + self.assertEqual(r["aninteger"], 42) def assertPatchResponse(self, response, item_id, resource=None): - id_field = self.domain[resource or self.known_resource]['id_field'] + id_field = self.domain[resource or self.known_resource]["id_field"] self.assertTrue(STATUS in response) self.assertTrue(STATUS_OK in response[STATUS]) self.assertFalse(ISSUES in response) @@ -722,14 +710,12 @@ def assertPatchResponse(self, response, item_id, resource=None): self.assertEqual(response[id_field], item_id) self.assertTrue(LAST_UPDATED in response) self.assertTrue(ETAG in response) - self.assertTrue('_links' in response) - self.assertItemLink(response['_links'], item_id) + self.assertTrue("_links" in response) + self.assertItemLink(response["_links"], item_id) def patch(self, url, data, headers=[]): - headers.append(('Content-Type', 'application/json')) - r = self.test_client.patch(url, - data=json.dumps(data), - headers=headers) + headers.append(("Content-Type", "application/json")) + r = self.test_client.patch(url, data=json.dumps(data), headers=headers) return self.parse_response(r) @@ -752,6 +738,7 @@ def test_on_pre_PATCH_contacts(self): def test_on_PATCH_dynamic_filter(self): def filter_this(resource, request, lookup): lookup["_id"] = self.unknown_item_id + self.app.on_pre_PATCH += filter_this # Would normally patch the known document; will return 404 instead. r, s = self.parse_response(self.patch()) @@ -801,14 +788,12 @@ def test_on_updated_contacts(self): def before_update(self): db = self.connection[MONGO_DBNAME] contact = db.contacts.find_one(ObjectId(self.item_id)) - return contact['ref'] == self.item_name + return contact["ref"] == self.item_name def after_update(self): return not self.before_update() def patch(self): - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] data = json.dumps({"ref": self.new_ref}) - return self.test_client.patch( - self.item_id_url, data=data, headers=headers) + return self.test_client.patch(self.item_id_url, data=data, headers=headers) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 30d3d4a50..da14beacb 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -33,16 +33,17 @@ def test_post_to_item_endpoint(self): def test_validation_error(self): r, status = self.post(self.known_resource_url, data={"ref": "123"}) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'ref': 'min length is 25'}) + self.assertValidationError(r, {"ref": "min length is 25"}) r, status = self.post(self.known_resource_url, data={"prog": 123}) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'ref': 'required'}) + self.assertValidationError(r, {"ref": "required"}) def test_post_bulk_insert_on_disabled_bulk(self): r, status = self.post( self.disabled_bulk_url, - data=[{'string_field': '123'}, {'string_field': '123'}]) + data=[{"string_field": "123"}, {"string_field": "123"}], + ) self.assert400(status) def test_post_empty_bulk_insert(self): @@ -58,120 +59,117 @@ def test_post_empty_resource(self): self.assertPostResponse(r) def test_post_string(self): - test_field = 'ref' + test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_duplicate_key(self): - data = {'ref': '1234567890123456789054321'} + data = {"ref": "1234567890123456789054321"} r = self.perform_post(data) - id_field = self.domain[self.known_resource]['id_field'] + id_field = self.domain[self.known_resource]["id_field"] item_id = r[id_field] - data = {'ref': '0123456789012345678901234', id_field: item_id} + data = {"ref": "0123456789012345678901234", id_field: item_id} r, status = self.post(self.known_resource_url, data=data) self.assertEqual(status, 409) def test_post_integer(self): - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'prog' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "prog" test_value = 1 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_list_as_array(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "role" test_value = ["vendor", "client"] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_rows(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "rows" - test_value = [ - {'sku': 'AT1234', 'price': 99}, - {'sku': 'XF9876', 'price': 9999} - ] + test_value = [{"sku": "AT1234", "price": 99}, {"sku": "XF9876", "price": 9999}] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_list(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "alist" test_value = ["a_string", 99] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_integer_zero(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "aninteger" test_value = 0 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_float_zero(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "afloat" test_value = 0.0 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_dict(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "location" - test_value = {'address': 'an address', 'city': 'a city'} + test_value = {"address": "an address", "city": "a city"} data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_datetime(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "born" test_value = "Tue, 06 Nov 2012 10:33:31 GMT" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_objectid(self): - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'tid' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "tid" test_value = "50656e4538345b39dd0414f0" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_null_objectid(self): # verify that #341 is fixed. - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'tid' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "tid" test_value = None data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_default_value(self): - test_field = 'title' + test_field = "title" test_value = "Mr." - data = {'ref': '9234567890123456789054321'} + data = {"ref": "9234567890123456789054321"} self.assertPostItem(data, test_field, test_value) def test_post_default_value_none(self): # default values that assimilate to None (0, '', False) were ignored # prior to 0.1.1 - title = self.domain['contacts']['schema']['title'] - title['default'] = '' + title = self.domain["contacts"]["schema"]["title"] + title["default"] = "" self.app.set_defaults() data = {"ref": "UUUUUUUUUUUUUUUUUUUUUUUUU"} - self.assertPostItem(data, 'title', '') + self.assertPostItem(data, "title", "") - title['type'] = 'integer' - title['default'] = 0 + title["type"] = "integer" + title["default"] = 0 self.app.set_defaults() data = {"ref": "TTTTTTTTTTTTTTTTTTTTTTTTT"} - self.assertPostItem(data, 'title', 0) + self.assertPostItem(data, "title", 0) - title['type'] = 'boolean' - title['default'] = False + title["type"] = "boolean" + title["default"] = False self.app.set_defaults() data = {"ref": "QQQQQQQQQQQQQQQQQQQQQQQQQ"} - self.assertPostItem(data, 'title', False) + self.assertPostItem(data, "title", False) def test_multi_post_valid(self): data = [ @@ -180,13 +178,13 @@ def test_multi_post_valid(self): ] r, status = self.post(self.known_resource_url, data=data) self.assert201(status) - results = r['_items'] + results = r["_items"] - self.assertEqual(results[0]['_status'], 'OK') - self.assertEqual(results[1]['_status'], 'OK') + self.assertEqual(results[0]["_status"], "OK") + self.assertEqual(results[1]["_status"], "OK") with self.app.test_request_context(): - contacts = self.app.data.driver.db['contacts'] + contacts = self.app.data.driver.db["contacts"] r = contacts.find({"ref": "9234567890123456789054321"}).count() self.assertTrue(r == 1) r = contacts.find({"ref": "5432112345678901234567890"}).count() @@ -202,23 +200,23 @@ def test_multi_post_invalid(self): ] r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) - results = r['_items'] + results = r["_items"] - self.assertEqual(results[0]['_status'], 'OK') - self.assertEqual(results[2]['_status'], 'OK') + self.assertEqual(results[0]["_status"], "OK") + self.assertEqual(results[2]["_status"], "OK") - self.assertValidationError(results[1], {'ref': 'required'}) - self.assertValidationError(results[3], {'ref': 'unique'}) - self.assertValidationError(results[4], {'tid': 'objectid'}) + self.assertValidationError(results[1], {"ref": "required"}) + self.assertValidationError(results[3], {"ref": "unique"}) + self.assertValidationError(results[4], {"tid": "objectid"}) - id_field = self.domain[self.known_resource]['id_field'] + id_field = self.domain[self.known_resource]["id_field"] self.assertTrue(id_field not in results[0]) self.assertTrue(id_field not in results[1]) self.assertTrue(id_field not in results[2]) self.assertTrue(id_field not in results[3]) with self.app.test_request_context(): - contacts = self.app.data.driver.db['contacts'] + contacts = self.app.data.driver.db["contacts"] r = contacts.find({"prog": 9999}).count() self.assertTrue(r == 0) r = contacts.find({"ref": "9234567890123456789054321"}).count() @@ -228,195 +226,187 @@ def test_post_x_www_form_urlencoded(self): test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} - r, status = self.parse_response(self.test_client.post( - self.known_resource_url, data=data)) + r, status = self.parse_response( + self.test_client.post(self.known_resource_url, data=data) + ) self.assert201(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) self.assertPostResponse(r) def test_post_x_www_form_urlencoded_number_serialization(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) test_field = "anumber" test_value = 34 data = {test_field: test_value} - r, status = self.parse_response(self.test_client.post( - self.known_resource_url, data=data)) + r, status = self.parse_response( + self.test_client.post(self.known_resource_url, data=data) + ) self.assert201(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) self.assertPostResponse(r) def test_post_auto_collapse_multiple_keys(self): - self.app.config['AUTO_COLLAPSE_MULTI_KEYS'] = True - self.app.register_resource('test_res', { - 'schema': { - 'list_field': { - 'type': 'list', - 'schema': { - 'type': 'string' - } - } - } - }) - - data = MultiDict([("list_field", "value1"), - ("list_field", "value2")]) + self.app.config["AUTO_COLLAPSE_MULTI_KEYS"] = True + self.app.register_resource( + "test_res", + {"schema": {"list_field": {"type": "list", "schema": {"type": "string"}}}}, + ) + + data = MultiDict([("list_field", "value1"), ("list_field", "value2")]) resp = self.test_client.post( - '/test_res/', data=data, - content_type='application/x-www-form-urlencoded') + "/test_res/", data=data, content_type="application/x-www-form-urlencoded" + ) r, status = self.parse_response(resp) self.assert201(status) - resp = self.test_client.post('/test_res/', data=data, - content_type='multipart/form-data') + resp = self.test_client.post( + "/test_res/", data=data, content_type="multipart/form-data" + ) r, status = self.parse_response(resp) self.assert201(status) def test_post_auto_collapse_media_list(self): - self.app.config['AUTO_COLLAPSE_MULTI_KEYS'] = True - self.app.register_resource('test_res', { - 'schema': { - 'list_field': { - 'type': 'list', - 'schema': { - 'type': 'media' - } - } - } - }) + self.app.config["AUTO_COLLAPSE_MULTI_KEYS"] = True + self.app.register_resource( + "test_res", + {"schema": {"list_field": {"type": "list", "schema": {"type": "media"}}}}, + ) # Create a document - data = MultiDict([('list_field', - (BytesIO(b'file_content1'), 'test1.txt')), - ('list_field', - (BytesIO(b'file_content2'), 'test2.txt'))]) - resp = self.test_client.post('/test_res/', data=data, - content_type='multipart/form-data') + data = MultiDict( + [ + ("list_field", (BytesIO(b"file_content1"), "test1.txt")), + ("list_field", (BytesIO(b"file_content2"), "test2.txt")), + ] + ) + resp = self.test_client.post( + "/test_res/", data=data, content_type="multipart/form-data" + ) r, status = self.parse_response(resp) self.assert201(status) # check that the files were created _db = self.connection[MONGO_DBNAME] - id_field = self.domain['test_res']['id_field'] + id_field = self.domain["test_res"]["id_field"] obj = _db.test_res.find_one({id_field: ObjectId(r[id_field])}) - media_ids = obj['list_field'] + media_ids = obj["list_field"] self.assertEqual(len(media_ids), 2) with self.app.test_request_context(): for i in [0, 1]: - self.assertTrue( - self.app.media.exists(media_ids[i], 'test_res')) + self.assertTrue(self.app.media.exists(media_ids[i], "test_res")) # GET the document and check the file content is correct r, status = self.parse_response( - self.test_client.get('/test_res/%s' % r[id_field])) - files = r['list_field'] - self.assertEqual(b64decode(files[0]), b'file_content1') - self.assertEqual(b64decode(files[1]), b'file_content2') + self.test_client.get("/test_res/%s" % r[id_field]) + ) + files = r["list_field"] + self.assertEqual(b64decode(files[0]), b"file_content1") + self.assertEqual(b64decode(files[1]), b"file_content2") # DELETE the document - resp = self.test_client.delete('/test_res/%s' % r['_id'], - headers={'If-Match': r['_etag']}) + resp = self.test_client.delete( + "/test_res/%s" % r["_id"], headers={"If-Match": r["_etag"]} + ) r, status = self.parse_response(resp) self.assert204(status) # Check files were deleted with self.app.test_request_context(): for i in [0, 1]: - self.assertFalse( - self.app.media.exists(media_ids[i], 'test_res')) + self.assertFalse(self.app.media.exists(media_ids[i], "test_res")) def test_post_auto_create_lists(self): - self.app.config['AUTO_CREATE_LISTS'] = True - self.app.register_resource('test_res', { - 'schema': { - 'list_field': { - 'type': 'list', - 'schema': { - 'type': 'string' - } - } - } - }) + self.app.config["AUTO_CREATE_LISTS"] = True + self.app.register_resource( + "test_res", + {"schema": {"list_field": {"type": "list", "schema": {"type": "string"}}}}, + ) data = MultiDict([("list_field", "value1")]) resp = self.test_client.post( - '/test_res/', data=data, - content_type='application/x-www-form-urlencoded') + "/test_res/", data=data, content_type="application/x-www-form-urlencoded" + ) r, status = self.parse_response(resp) self.assert201(status) def test_post_decimal_number_success(self): data = {"decimal_number": 100} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) self.assertPostResponse(r) - id_field = self.domain['invoices']['id_field'] + id_field = self.domain["invoices"]["id_field"] unique_id = r[id_field] - r, status = self.get('invoices/%s' % unique_id) + r, status = self.get("invoices/%s" % unique_id) self.assert200(status) assert isinstance(r["decimal_number"], str_type) def test_post_decimal_number_fail(self): data = {"decimal_number": "100.0.0"} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert422(status) def test_post_referential_integrity(self): data = {"person": self.unknown_item_id} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) - self.assertValidationError(r, {'person': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) + self.assertValidationError(r, {"person": expected}) data = {"person": self.item_id} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) self.assertPostResponse(r) def test_dbref_post_referential_integrity(self): - data = {"persondbref": {"$col": "contacts", - "$id": self.unknown_item_id}} - r, status = self.post('/invoices/', data=data) + data = {"persondbref": {"$col": "contacts", "$id": self.unknown_item_id}} + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) - self.assertValidationError(r, {'persondbref': expected}) + self.assertValidationError(r, {"persondbref": expected}) data = {"persondbref": {"$col": "contacts", "$id": self.item_id}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) self.assertPostResponse(r) def test_post_referential_integrity_list(self): data = {"invoicing_contacts": [self.item_id, self.unknown_item_id]} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) - self.assertValidationError(r, {'invoicing_contacts': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) + self.assertValidationError(r, {"invoicing_contacts": expected}) data = {"invoicing_contacts": [self.item_id, self.item_id]} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) self.assertPostResponse(r) def test_post_allow_unknown(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) data = {"unknown": "unknown"} r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'unknown': 'unknown'}) + self.assertValidationError(r, {"unknown": "unknown"}) # since resource settings are only set at app startup we set # those that influence the 'allow_unknown' property by hand (so we # don't have to re-initialize the whole app.) - settings = self.app.config['DOMAIN'][self.known_resource] - settings['allow_unknown'] = True - settings['datasource']['projection'] = {} + settings = self.app.config["DOMAIN"][self.known_resource] + settings["allow_unknown"] = True + settings["datasource"]["projection"] = {} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) @@ -424,54 +414,54 @@ def test_post_allow_unknown(self): # test that the unknown field is also returned with subsequent get # requests - id = r[self.domain[self.known_resource]['id_field']] - r = self.test_client.get('%s/%s' % (self.known_resource_url, id)) + id = r[self.domain[self.known_resource]["id_field"]] + r = self.test_client.get("%s/%s" % (self.known_resource_url, id)) r_data = json.loads(r.get_data()) - self.assertTrue('unknown' in r_data) - self.assertEqual('unknown', r_data['unknown']) + self.assertTrue("unknown" in r_data) + self.assertEqual("unknown", r_data["unknown"]) def test_post_with_content_type_charset(self): - test_field = 'ref' + test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} - r, status = self.post(self.known_resource_url, data=data, - content_type='application/json; charset=utf-8') + r, status = self.post( + self.known_resource_url, + data=data, + content_type="application/json; charset=utf-8", + ) self.assert201(status) self.assertPostResponse(r) def test_post_with_extra_response_fields(self): - self.domain['contacts']['extra_response_fields'] = ['ref', 'notreally'] - test_field = 'ref' + self.domain["contacts"]["extra_response_fields"] = ["ref", "notreally"] + test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) - self.assertTrue('ref' in r and 'notreally' not in r) + self.assertTrue("ref" in r and "notreally" not in r) def test_post_with_excluded_response_fields(self): - data = { - 'email': 'test@email.com', - 'password': 'password' - } - r, status = self.post('login', data=data) + data = {"email": "test@email.com", "password": "password"} + r, status = self.post("login", data=data) self.assert201(status) - login_id = r[self.domain['login']['id_field']] - r = self.test_client.get('%s/%s' % ('login', login_id)) + login_id = r[self.domain["login"]["id_field"]] + r = self.test_client.get("%s/%s" % ("login", login_id)) r_data = json.loads(r.get_data()) - self.assertTrue('password' not in r_data) - self.assertTrue('email' in r_data) + self.assertTrue("password" not in r_data) + self.assertTrue("email" in r_data) def test_post_write_concern(self): # should get a 500 since there's no replicaset on mongod test instance - self.domain['contacts']['mongo_write_concern'] = {'w': 2} - test_field = 'ref' + self.domain["contacts"]["mongo_write_concern"] = {"w": 2} + test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} _, status = self.post(self.known_resource_url, data=data) self.assert500(status) # 0 and 1 are the only valid values for 'w' on our mongod instance - self.domain['contacts']['mongo_write_concern'] = {'w': 0} + self.domain["contacts"]["mongo_write_concern"] = {"w": 0} test_value = "1234567890123456789054329" data = {test_field: test_value} _, status = self.post(self.known_resource_url, data=data) @@ -479,115 +469,118 @@ def test_post_write_concern(self): def test_post_with_get_override(self): # a GET request with POST override turns into a POST request. - test_field = 'ref' + test_field = "ref" test_value = "1234567890123456789054321" data = json.dumps({test_field: test_value}) - headers = [('X-HTTP-Method-Override', 'POST'), - ('Content-Type', 'application/json')] - r = self.test_client.get(self.known_resource_url, data=data, - headers=headers) + headers = [ + ("X-HTTP-Method-Override", "POST"), + ("Content-Type", "application/json"), + ] + r = self.test_client.get(self.known_resource_url, data=data, headers=headers) self.assert201(r.status_code) self.assertPostResponse(json.loads(r.get_data())) def test_post_list_of_objectid(self): - objectid = '50656e4538345b39dd0414f0' - del(self.domain['contacts']['schema']['ref']['required']) - data = {'id_list': ['%s' % objectid]} + objectid = "50656e4538345b39dd0414f0" + del (self.domain["contacts"]["schema"]["ref"]["required"]) + data = {"id_list": ["%s" % objectid]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) - r, status = self.get(self.known_resource, '?where={"id_list": ' - '{"$in": ["%s"]}}' % objectid) + r, status = self.get( + self.known_resource, '?where={"id_list": ' '{"$in": ["%s"]}}' % objectid + ) self.assert200(status) self.assertTrue(len(r), 1) - self.assertTrue('%s' % objectid in r['_items'][0]['id_list']) + self.assertTrue("%s" % objectid in r["_items"][0]["id_list"]) def test_post_nested_dict_objectid(self): - objectid = '50656e4538345b39dd0414f0' - del(self.domain['contacts']['schema']['ref']['required']) - data = {'id_list_of_dict': [{'id': '%s' % objectid}]} + objectid = "50656e4538345b39dd0414f0" + del (self.domain["contacts"]["schema"]["ref"]["required"]) + data = {"id_list_of_dict": [{"id": "%s" % objectid}]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) - r, status = self.get(self.known_resource, - '?where={"id_list_of_dict.id": ' '"%s"}' - % objectid) + r, status = self.get( + self.known_resource, '?where={"id_list_of_dict.id": ' '"%s"}' % objectid + ) self.assertTrue(len(r), 1) - self.assertTrue('%s' % objectid in - r['_items'][0]['id_list_of_dict'][0]['id']) + self.assertTrue("%s" % objectid in r["_items"][0]["id_list_of_dict"][0]["id"]) def test_post_valueschema_with_objectid(self): - del(self.domain['contacts']['schema']['ref']['required']) - data = {'dict_valueschema': {'id': {'challenge': - '50656e4538345b39dd0414f0'}}} + del (self.domain["contacts"]["schema"]["ref"]["required"]) + data = {"dict_valueschema": {"id": {"challenge": "50656e4538345b39dd0414f0"}}} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) def test_post_list_fixed_len(self): - objectid = '50656e4538345b39dd0414f0' - del(self.domain['contacts']['schema']['ref']['required']) - data = {'id_list_fixed_len': ['%s' % objectid]} + objectid = "50656e4538345b39dd0414f0" + del (self.domain["contacts"]["schema"]["ref"]["required"]) + data = {"id_list_fixed_len": ["%s" % objectid]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) - r, status = self.get(self.known_resource, - '?where={"id_list_fixed_len": ' - '{"$in": ["%s"]}}' % objectid) + r, status = self.get( + self.known_resource, + '?where={"id_list_fixed_len": ' '{"$in": ["%s"]}}' % objectid, + ) self.assert200(status) self.assertTrue(len(r), 1) - self.assertTrue('%s' % objectid in r['_items'][0]['id_list_fixed_len']) + self.assertTrue("%s" % objectid in r["_items"][0]["id_list_fixed_len"]) def test_custom_issues(self): - self.app.config['ISSUES'] = 'errors' + self.app.config["ISSUES"] = "errors" r, status = self.post(self.known_resource_url, data={"ref": "123"}) self.assertValidationErrorStatus(status) - self.assertTrue('errors' in r and ISSUES not in r) + self.assertTrue("errors" in r and ISSUES not in r) def test_custom_status(self): - self.app.config['STATUS'] = 'report' + self.app.config["STATUS"] = "report" r, status = self.post(self.known_resource_url, data={"ref": "123"}) self.assertValidationErrorStatus(status) - self.assertTrue('report' in r and STATUS not in r) + self.assertTrue("report" in r and STATUS not in r) def test_custom_etag_update_date(self): - self.app.config['ETAG'] = '_myetag' - r, status = self.post(self.known_resource_url, - data={"ref": "1234567890123456789054321"}) + self.app.config["ETAG"] = "_myetag" + r, status = self.post( + self.known_resource_url, data={"ref": "1234567890123456789054321"} + ) self.assert201(status) - self.assertTrue('_myetag' in r and ETAG not in r) + self.assertTrue("_myetag" in r and ETAG not in r) def test_custom_date_updated(self): - self.app.config['LAST_UPDATED'] = '_update_date' - r, status = self.post(self.known_resource_url, - data={"ref": "1234567890123456789054321"}) + self.app.config["LAST_UPDATED"] = "_update_date" + r, status = self.post( + self.known_resource_url, data={"ref": "1234567890123456789054321"} + ) self.assert201(status) - self.assertTrue('_update_date' in r and LAST_UPDATED not in r) + self.assertTrue("_update_date" in r and LAST_UPDATED not in r) def test_subresource(self): - response, status = self.post('users/%s/invoices' % - self.item_id, data={}) + response, status = self.post("users/%s/invoices" % self.item_id, data={}) self.assert201(status) self.assertPostResponse(response) - invoice_id = response.get(self.domain['peopleinvoices']['id_field']) - response, status = self.get('users/%s/invoices/%s' % - (self.item_id, invoice_id)) + invoice_id = response.get(self.domain["peopleinvoices"]["id_field"]) + response, status = self.get("users/%s/invoices/%s" % (self.item_id, invoice_id)) self.assert200(status) - self.assertEqual(response.get('person'), self.item_id) + self.assertEqual(response.get("person"), self.item_id) def test_subresource_required_ref(self): - response, status = self.post('users/%s/required_invoices' % - self.item_id, data={}) + response, status = self.post( + "users/%s/required_invoices" % self.item_id, data={} + ) self.assert201(status) self.assertPostResponse(response) - invoice_id = response.get(self.domain['required_invoices']['id_field']) - response, status = self.get('users/%s/required_invoices/%s' % - (self.item_id, invoice_id)) + invoice_id = response.get(self.domain["required_invoices"]["id_field"]) + response, status = self.get( + "users/%s/required_invoices/%s" % (self.item_id, invoice_id) + ) self.assert200(status) - self.assertEqual(response.get('person'), self.item_id) + self.assertEqual(response.get("person"), self.item_id) def test_post_ifmatch_disabled(self): # if IF_MATCH is disabled, then we get no etag in the payload. - self.app.config['IF_MATCH'] = False - test_field = 'ref' + self.app.config["IF_MATCH"] = False + test_field = "ref" test_value = "1234567890123456789054321" data = {test_field: test_value} r, status = self.post(self.known_resource_url, data=data) @@ -595,57 +588,57 @@ def test_post_ifmatch_disabled(self): def test_post_custom_idfield(self): # Test that we can post a document with a custom id_field. - id_field = 'sku' - product = {id_field: 'FOO', 'title': 'Foobar'} - r, status = self.post('products', data=product) + id_field = "sku" + product = {id_field: "FOO", "title": "Foobar"} + r, status = self.post("products", data=product) self.assert201(status) self.assertTrue(id_field in r) - self.assertItemLink(r['_links'], r[id_field]) + self.assertItemLink(r["_links"], r[id_field]) def test_post_with_relation_to_custom_idfield(self): # Test that we can post a document that relates to a resource with a # custom id_field. - id_field = 'sku' + id_field = "sku" db = self.connection[MONGO_DBNAME] existing_product = db.products.find_one() product = { - id_field: 'BAR', - 'title': 'Foobar', - 'parent_product': existing_product[id_field] + id_field: "BAR", + "title": "Foobar", + "parent_product": existing_product[id_field], } - r, status = self.post('products', data=product) + r, status = self.post("products", data=product) self.assert201(status) self.assertTrue(id_field in r) - self.assertItemLink(r['_links'], r[id_field]) - r, status = self.get('products', item='BAR') - self.assertEqual(r['parent_product'], existing_product[id_field]) + self.assertItemLink(r["_links"], r[id_field]) + r, status = self.get("products", item="BAR") + self.assertEqual(r["parent_product"], existing_product[id_field]) def test_post_bandwidth_saver(self): - data = {'inv_number': self.random_string(10)} + data = {"inv_number": self.random_string(10)} # bandwidth_saver is on by default - self.assertTrue(self.app.config['BANDWIDTH_SAVER']) + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) r, status = self.post(self.empty_resource_url, data=data) self.assert201(status) self.assertPostResponse(r) - self.assertFalse('inv_number' in r) - etag = r[self.app.config['ETAG']] + self.assertFalse("inv_number" in r) + etag = r[self.app.config["ETAG"]] r, status = self.get( - self.empty_resource, '', - r[self.domain[self.empty_resource]['id_field']]) - self.assertEqual(etag, r[self.app.config['ETAG']]) + self.empty_resource, "", r[self.domain[self.empty_resource]["id_field"]] + ) + self.assertEqual(etag, r[self.app.config["ETAG"]]) # test return all fields (bandwidth_saver off) - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False r, status = self.post(self.empty_resource_url, data=data) self.assert201(status) self.assertPostResponse(r) - self.assertTrue('inv_number' in r) - etag = r[self.app.config['ETAG']] + self.assertTrue("inv_number" in r) + etag = r[self.app.config["ETAG"]] r, status = self.get( - self.empty_resource, '', - r[self.domain[self.empty_resource]['id_field']]) - self.assertEqual(etag, r[self.app.config['ETAG']]) + self.empty_resource, "", r[self.domain[self.empty_resource]["id_field"]] + ) + self.assertEqual(etag, r[self.app.config["ETAG"]]) def test_post_alternative_payload(self): payl = {"ref": "5432112345678901234567890", "role": ["agent"]} @@ -656,119 +649,106 @@ def test_post_alternative_payload(self): def test_post_dependency_fields_with_default(self): # test that default values are resolved before validation. See #353. - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'dependency_field2' - test_value = 'a value' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "dependency_field2" + test_value = "a value" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_dependency_required_fields(self): - del(self.domain['contacts']['schema']['ref']['required']) - schema = self.domain['contacts']['schema'] - schema['dependency_field3']['required'] = True + del (self.domain["contacts"]["schema"]["ref"]["required"]) + schema = self.domain["contacts"]["schema"] + schema["dependency_field3"]["required"] = True r, status = self.post(self.known_resource_url, data={}) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'dependency_field3': 'required'}) + self.assertValidationError(r, {"dependency_field3": "required"}) # required field dependnecy value matches the dependent field's default # value. validation still fails since required field is still missing. # See #665. - schema['dependency_field3']['dependencies'] = {'dependency_field1': - 'default'} + schema["dependency_field3"]["dependencies"] = {"dependency_field1": "default"} r, status = self.post(self.known_resource_url, data={}) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'dependency_field3': 'required'}) + self.assertValidationError(r, {"dependency_field3": "required"}) - r, status = self.post(self.known_resource_url, - data={'dependency_field3': 'hello'}) + r, status = self.post( + self.known_resource_url, data={"dependency_field3": "hello"} + ) self.assert201(status) def test_post_dependency_fields_with_values(self): # test that dependencies values are validated correctly. See #547. - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) schema = { - 'field1': { - 'required': False, - 'default': 'one' - }, - 'field2': { - 'required': True, - 'dependencies': {'field1': ['one', 'two']} - } + "field1": {"required": False, "default": "one"}, + "field2": {"required": True, "dependencies": {"field1": ["one", "two"]}}, } settings = { - 'RESOURCE_METHODS': ['GET', 'POST', 'DELETE'], - 'ITEM_METHODS': ['GET', 'PATCH', 'PUT', 'DELETE'], - 'schema': schema + "RESOURCE_METHODS": ["GET", "POST", "DELETE"], + "ITEM_METHODS": ["GET", "PATCH", "PUT", "DELETE"], + "schema": schema, } - self.app.register_resource('posts', settings) + self.app.register_resource("posts", settings) data = {"field1": "three", "field2": 7} - r, s = self.post('posts', data=data) + r, s = self.post("posts", data=data) self.assert422(s) data = {"field2": 7} - r, s = self.post('posts', data=data) + r, s = self.post("posts", data=data) self.assert201(s) data = {"field1": "one", "field2": 7} - r, s = self.post('posts', data=data) + r, s = self.post("posts", data=data) self.assert201(s) data = {"field1": "two", "field2": 7} - r, s = self.post('posts', data=data) + r, s = self.post("posts", data=data) self.assert201(s) def test_post_dependency_fields_with_subdocuments(self): # test that dependencies with sub-document fields are properly # validated. See #706. - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) schema = { - 'field1': { - 'type': 'dict', - 'schema': { - 'address': {'type': 'string'} - } - }, - 'field2': { - 'dependencies': {'field1.address': ['one', 'two']} - } + "field1": {"type": "dict", "schema": {"address": {"type": "string"}}}, + "field2": {"dependencies": {"field1.address": ["one", "two"]}}, } settings = { - 'RESOURCE_METHODS': ['GET', 'POST', 'DELETE'], - 'ITEM_METHODS': ['GET', 'PATCH', 'PUT', 'DELETE'], - 'schema': schema + "RESOURCE_METHODS": ["GET", "POST", "DELETE"], + "ITEM_METHODS": ["GET", "PATCH", "PUT", "DELETE"], + "schema": schema, } - self.app.register_resource('endpoint', settings) + self.app.register_resource("endpoint", settings) data = {"field1": {"address": "three"}, "field2": 7} - r, s = self.post('endpoint', data=data) + r, s = self.post("endpoint", data=data) self.assert422(s) data = {"field1": {"address": "one"}, "field2": 7} - r, s = self.post('endpoint', data=data) + r, s = self.post("endpoint", data=data) self.assert201(s) data = {"field1": {"address": "two"}, "field2": 7} - r, s = self.post('endpoint', data=data) + r, s = self.post("endpoint", data=data) self.assert201(s) def test_post_readonly_field_with_default(self): # test that a read only field with a 'default' setting is correctly # validated now that we resolve field values before validation. - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'read_only_field' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "read_only_field" # thou shalt not pass. - test_value = 'a random value' + test_value = "a random value" data = {test_field: test_value} r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) # this will not pass even if value matches 'default' setting. # (hey it's still a read-onlu field so you can't reset it) - test_value = 'default' + test_value = "default" data = {test_field: test_value} r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) @@ -776,132 +756,136 @@ def test_post_readonly_field_with_default(self): def test_post_readonly_in_dict(self): # Test that a post with a readonly field inside a dict is properly # validated (even if it has a defult value) - del(self.domain['contacts']['schema']['ref']['required']) - test_field = 'dict_with_read_only' - test_value = {'read_only_in_dict': 'default'} + del (self.domain["contacts"]["schema"]["ref"]["required"]) + test_field = "dict_with_read_only" + test_value = {"read_only_in_dict": "default"} data = {test_field: test_value} r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) def test_post_valueschema_dict(self): """ make sure Cerberus#48 is fixed """ - del(self.domain['contacts']['schema']['ref']['required']) - r, status = self.post(self.known_resource_url, - data={"valueschema_dict": {"k1": "1"}}) + del (self.domain["contacts"]["schema"]["ref"]["required"]) + r, status = self.post( + self.known_resource_url, data={"valueschema_dict": {"k1": "1"}} + ) self.assertValidationErrorStatus(status) issues = r[ISSUES] - self.assertTrue('valueschema_dict' in issues) - self.assertEqual(issues['valueschema_dict'], - {'k1': 'must be of integer type'}) + self.assertTrue("valueschema_dict" in issues) + self.assertEqual(issues["valueschema_dict"], {"k1": "must be of integer type"}) - r, status = self.post(self.known_resource_url, - data={"valueschema_dict": {"k1": 1}}) + r, status = self.post( + self.known_resource_url, data={"valueschema_dict": {"k1": 1}} + ) self.assert201(status) def test_post_keyschema_dict(self): - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) - r, status = self.post(self.known_resource_url, - data={"keyschema_dict": {"aaa": 1}}) + r, status = self.post( + self.known_resource_url, data={"keyschema_dict": {"aaa": 1}} + ) self.assert201(status) - r, status = self.post(self.known_resource_url, - data={"keyschema_dict": {"AAA": "1"}}) + r, status = self.post( + self.known_resource_url, data={"keyschema_dict": {"AAA": "1"}} + ) self.assertValidationErrorStatus(status) issues = r[ISSUES] - self.assertTrue('keyschema_dict' in issues) - self.assertEqual(issues['keyschema_dict'], - {'AAA': "value does not match regex '[a-z]+'"}) + self.assertTrue("keyschema_dict" in issues) + self.assertEqual( + issues["keyschema_dict"], {"AAA": "value does not match regex '[a-z]+'"} + ) def test_post_internal(self): # test that post_internal is available and working properly. - test_field = 'ref' + test_field = "ref" test_value = "1234567890123456789054321" payload = {test_field: test_value} with self.app.test_request_context(self.known_resource_url): - r, _, _, status, _ = post_internal(self.known_resource, - payl=payload) + r, _, _, status, _ = post_internal(self.known_resource, payl=payload) self.assert201(status) def test_post_internal_skip_validation(self): # test that when skip_validation is active everything behaves as # expected. Also make sure that #726 is fixed. - test_field = 'ref' + test_field = "ref" test_value = "1234567890123456789054321" payload = {test_field: test_value} with self.app.test_request_context(self.known_resource_url): - r, _, _, status, _ = post_internal(self.known_resource, - payl=payload, - skip_validation=True) + r, _, _, status, _ = post_internal( + self.known_resource, payl=payload, skip_validation=True + ) self.assert201(status) def test_post_nested(self): - del(self.domain['contacts']['schema']['ref']['required']) - data = {'location.city': 'a nested city', - 'location.address': 'a nested address'} + del (self.domain["contacts"]["schema"]["ref"]["required"]) + data = { + "location.city": "a nested city", + "location.address": "a nested address", + } r, status = self.post(self.known_resource_url, data=data) self.assert201(status) values = self.compare_post_with_get( - r[self.domain[self.known_resource]['id_field']], - ['location']).pop() - self.assertEqual(values['city'], 'a nested city') - self.assertEqual(values['address'], 'a nested address') + r[self.domain[self.known_resource]["id_field"]], ["location"] + ).pop() + self.assertEqual(values["city"], "a nested city") + self.assertEqual(values["address"], "a nested address") def test_post_error_as_list(self): - del(self.domain['contacts']['schema']['ref']['required']) - self.app.config['VALIDATION_ERROR_AS_LIST'] = True - data = {'unknown_field': 'a value'} + del (self.domain["contacts"]["schema"]["ref"]["required"]) + self.app.config["VALIDATION_ERROR_AS_LIST"] = True + data = {"unknown_field": "a value"} r, status = self.post(self.known_resource_url, data=data) self.assert422(status) - error = r[ISSUES]['unknown_field'] + error = r[ISSUES]["unknown_field"] self.assertTrue(isinstance(error, list)) def test_id_field_included_with_document(self): # since v0.6 we also allow the id field to be included with the POSTed # document - id_field = self.domain[self.known_resource]['id_field'] - id = '55b2340538345bd048100ffe' + id_field = self.domain[self.known_resource]["id_field"] + id = "55b2340538345bd048100ffe" data = {"ref": "1234567890123456789054321", id_field: id} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) self.assertPostResponse(r) - self.assertEqual(r['_id'], id) + self.assertEqual(r["_id"], id) def test_post_type_coercion(self): - schema = self.domain[self.known_resource]['schema'] - schema['aninteger']['coerce'] = lambda string: int(float(string)) - data = {'ref': '1234567890123456789054321', 'aninteger': '42.3'} - self.assertPostItem(data, 'aninteger', 42) + schema = self.domain[self.known_resource]["schema"] + schema["aninteger"]["coerce"] = lambda string: int(float(string)) + data = {"ref": "1234567890123456789054321", "aninteger": "42.3"} + self.assertPostItem(data, "aninteger", 42) def test_post_location_header_hateoas_on(self): - self.app.config['HATEOAS'] = True - data = json.dumps({'ref': '1234567890123456789054321'}) - headers = [('Content-Type', 'application/json')] - r = self.test_client.post(self.known_resource_url, data=data, - headers=headers) - self.assertTrue('Location' in r.headers) - self.assertTrue(self.known_resource_url in r.headers['Location']) + self.app.config["HATEOAS"] = True + data = json.dumps({"ref": "1234567890123456789054321"}) + headers = [("Content-Type", "application/json")] + r = self.test_client.post(self.known_resource_url, data=data, headers=headers) + self.assertTrue("Location" in r.headers) + self.assertTrue(self.known_resource_url in r.headers["Location"]) def test_post_location_header_hateoas_off(self): - self.app.config['HATEOAS'] = False - data = json.dumps({'ref': '1234567890123456789054321'}) - headers = [('Content-Type', 'application/json')] - r = self.test_client.post(self.known_resource_url, data=data, - headers=headers) - self.assertTrue('Location' in r.headers) - self.assertTrue(self.known_resource_url in r.headers['Location']) + self.app.config["HATEOAS"] = False + data = json.dumps({"ref": "1234567890123456789054321"}) + headers = [("Content-Type", "application/json")] + r = self.test_client.post(self.known_resource_url, data=data, headers=headers) + self.assertTrue("Location" in r.headers) + self.assertTrue(self.known_resource_url in r.headers["Location"]) def test_post_custom_json_content_type(self): - data = {'ref': '1234567890123456789054321'} - r, status = self.post(self.known_resource_url, data, - content_type='application/csp-report') + data = {"ref": "1234567890123456789054321"} + r, status = self.post( + self.known_resource_url, data, content_type="application/csp-report" + ) self.assert400(status) - self.app.config['JSON_REQUEST_CONTENT_TYPES'] += \ - ['application/csp-report'] - r, status = self.post(self.known_resource_url, data, - content_type='application/csp-report') + self.app.config["JSON_REQUEST_CONTENT_TYPES"] += ["application/csp-report"] + r, status = self.post( + self.known_resource_url, data, content_type="application/csp-report" + ) self.assert201(status) def perform_post(self, data, valid_items=[0]): @@ -912,19 +896,19 @@ def perform_post(self, data, valid_items=[0]): def assertPostItem(self, data, test_field, test_value): r = self.perform_post(data) - item_id = r[self.domain[self.known_resource]['id_field']] + item_id = r[self.domain[self.known_resource]["id_field"]] item_etag = r[ETAG] db_value = self.compare_post_with_get(item_id, [test_field, ETAG]) self.assertTrue(db_value[0] == test_value) self.assertTrue(db_value[1] == item_etag) def assertPostResponse(self, response, valid_items=[0], resource=None): - if '_items' in response: - results = response['_items'] + if "_items" in response: + results = response["_items"] else: results = [response] - id_field = self.domain[resource or self.known_resource]['id_field'] + id_field = self.domain[resource or self.known_resource]["id_field"] for i in valid_items: item = results[i] @@ -933,15 +917,14 @@ def assertPostResponse(self, response, valid_items=[0], resource=None): self.assertFalse(ISSUES in item) self.assertTrue(id_field in item) self.assertTrue(LAST_UPDATED in item) - self.assertTrue('_links' in item) - self.assertItemLink(item['_links'], item[id_field]) + self.assertTrue("_links" in item) + self.assertItemLink(item["_links"], item[id_field]) self.assertTrue(ETAG in item) def compare_post_with_get(self, item_id, fields): - raw_r = self.test_client.get("%s/%s" % (self.known_resource_url, - item_id)) + raw_r = self.test_client.get("%s/%s" % (self.known_resource_url, item_id)) item, status = self.parse_response(raw_r) - id_field = self.domain[self.known_resource]['id_field'] + id_field = self.domain[self.known_resource]["id_field"] self.assert200(status) self.assertTrue(id_field in item) self.assertTrue(item[id_field] == item_id) @@ -953,10 +936,10 @@ def compare_post_with_get(self, item_id, fields): else: return item[fields] - def post(self, url, data, headers=None, content_type='application/json'): + def post(self, url, data, headers=None, content_type="application/json"): if not headers: headers = [] - headers.append(('Content-Type', content_type)) + headers.append(("Content-Type", content_type)) r = self.test_client.post(url, data=json.dumps(data), headers=headers) return self.parse_response(r) @@ -993,32 +976,31 @@ def test_on_insert(self): self.app.on_insert += devent self.post() self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.new_contact_id, devent.called[1][0]['ref']) + self.assertEqual(self.new_contact_id, devent.called[1][0]["ref"]) def test_on_insert_contacts(self): devent = DummyEvent(self.before_insert, True) self.app.on_insert_contacts += devent self.post() - self.assertEqual(self.new_contact_id, devent.called[0][0]['ref']) + self.assertEqual(self.new_contact_id, devent.called[0][0]["ref"]) def test_on_inserted(self): devent = DummyEvent(self.after_insert, True) self.app.on_inserted += devent self.post() self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.new_contact_id, devent.called[1][0]['ref']) + self.assertEqual(self.new_contact_id, devent.called[1][0]["ref"]) def test_on_inserted_contacts(self): devent = DummyEvent(self.after_insert, True) self.app.on_inserted_contacts += devent self.post() - self.assertEqual(self.new_contact_id, devent.called[0][0]['ref']) + self.assertEqual(self.new_contact_id, devent.called[0][0]["ref"]) def post(self): - headers = [('Content-Type', 'application/json')] + headers = [("Content-Type", "application/json")] data = json.dumps({"ref": self.new_contact_id}) - self.test_client.post( - self.known_resource_url, data=data, headers=headers) + self.test_client.post(self.known_resource_url, data=data, headers=headers) def before_insert(self): db = self.connection[MONGO_DBNAME] diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index fdd3511f4..43e60990b 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -24,66 +24,73 @@ def test_readonly_resource(self): self.assert405(status) def test_by_name(self): - _, status = self.put(self.item_name_url, data={'key1': 'value1'}) + _, status = self.put(self.item_name_url, data={"key1": "value1"}) self.assert405(status) def test_ifmatch_missing(self): - _, status = self.put(self.item_id_url, data={'key1': 'value1'}) + _, status = self.put(self.item_id_url, data={"key1": "value1"}) self.assert428(status) def test_ifmatch_missing_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False + self.app.config["ENFORCE_IF_MATCH"] = False def test_ifmatch_disabled(self): - self.app.config['IF_MATCH'] = False - r, status = self.put(self.item_id_url, - data={'ref': '1234567890123456789012345'}) + self.app.config["IF_MATCH"] = False + r, status = self.put( + self.item_id_url, data={"ref": "1234567890123456789012345"} + ) self.assert200(status) self.assertTrue(ETAG not in r) def test_ifmatch_disabled_enforce_ifmatch_disabled(self): - self.app.config['IF_MATCH'] = False - self.app.config['ENFORCE_IF_MATCH'] = False + self.app.config["IF_MATCH"] = False + self.app.config["ENFORCE_IF_MATCH"] = False r, status = self.put( - self.item_id_url, - data={'ref': '1234567890123456789012345'} + self.item_id_url, data={"ref": "1234567890123456789012345"} ) self.assert200(status) self.assertTrue(ETAG not in r) def test_ifmatch_bad_etag(self): - _, status = self.put(self.item_id_url, - data={'key1': 'value1'}, - headers=[('If-Match', 'not-quite-right')]) + _, status = self.put( + self.item_id_url, + data={"key1": "value1"}, + headers=[("If-Match", "not-quite-right")], + ) self.assert412(status) def test_ifmatch_bad_etag_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False + self.app.config["ENFORCE_IF_MATCH"] = False _, status = self.put( self.item_id_url, - data={'key1': 'value1'}, - headers=[('If-Match', 'not-quite-right')] + data={"key1": "value1"}, + headers=[("If-Match", "not-quite-right")], ) self.assert412(status) def test_unique_value(self): - r, status = self.put(self.item_id_url, - data={"ref": "%s" % self.alt_ref}, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, + data={"ref": "%s" % self.alt_ref}, + headers=[("If-Match", self.item_etag)], + ) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'ref': "value '%s' is not unique" % - self.alt_ref}) + self.assertValidationError( + r, {"ref": "value '%s' is not unique" % self.alt_ref} + ) def test_allow_unknown(self): changes = {"unknown": "unknown"} - r, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'unknown': 'unknown field'}) - self.app.config['DOMAIN'][self.known_resource]['allow_unknown'] = True + self.assertValidationError(r, {"unknown": "unknown field"}) + self.app.config["DOMAIN"][self.known_resource]["allow_unknown"] = True changes = {"unknown": "unknown", "ref": "1234567890123456789012345"} - r, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) self.assertPutResponse(r, self.item_id) @@ -91,72 +98,80 @@ def test_put_x_www_form_urlencoded(self): field = "ref" test_value = "1234567890123456789012345" changes = {field: test_value} - headers = [('If-Match', self.item_etag)] - r, status = self.parse_response(self.test_client.put( - self.item_id_url, data=changes, headers=headers)) + headers = [("If-Match", self.item_etag)] + r, status = self.parse_response( + self.test_client.put(self.item_id_url, data=changes, headers=headers) + ) self.assert200(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) def test_put_x_www_form_urlencoded_number_serialization(self): - del(self.domain['contacts']['schema']['ref']['required']) - field = 'anumber' + del (self.domain["contacts"]["schema"]["ref"]["required"]) + field = "anumber" test_value = 41 changes = {field: test_value} - headers = [('If-Match', self.item_etag)] - r, status = self.parse_response(self.test_client.put( - self.item_id_url, data=changes, headers=headers)) + headers = [("If-Match", self.item_etag)] + r, status = self.parse_response( + self.test_client.put(self.item_id_url, data=changes, headers=headers) + ) self.assert200(status) - self.assertTrue('OK' in r[STATUS]) + self.assertTrue("OK" in r[STATUS]) def test_put_referential_integrity(self): data = {"person": self.unknown_item_id} - headers = [('If-Match', self.invoice_etag)] + headers = [("If-Match", self.invoice_etag)] r, status = self.put(self.invoice_id_url, data=data, headers=headers) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) - self.assertValidationError(r, {'person': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) + self.assertValidationError(r, {"person": expected}) data = {"person": self.item_id} r, status = self.put(self.invoice_id_url, data=data, headers=headers) self.assert200(status) - self.assertPutResponse(r, self.invoice_id, 'invoices') + self.assertPutResponse(r, self.invoice_id, "invoices") def test_put_referential_integrity_list(self): data = {"invoicing_contacts": [self.item_id, self.unknown_item_id]} - headers = [('If-Match', self.invoice_etag)] + headers = [("If-Match", self.invoice_etag)] r, status = self.put(self.invoice_id_url, data=data, headers=headers) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.domain['contacts']['id_field'])) - self.assertValidationError(r, {'invoicing_contacts': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.domain["contacts"]["id_field"], + ) + self.assertValidationError(r, {"invoicing_contacts": expected}) data = {"invoicing_contacts": [self.item_id, self.item_id]} r, status = self.put(self.invoice_id_url, data=data, headers=headers) self.assert200(status) - self.assertPutResponse(r, self.invoice_id, 'invoices') + self.assertPutResponse(r, self.invoice_id, "invoices") def test_put_write_concern_success(self): # 0 and 1 are the only valid values for 'w' on our mongod instance (1 # is the default) - self.domain['contacts']['mongo_write_concern'] = {'w': 0} + self.domain["contacts"]["mongo_write_concern"] = {"w": 0} field = "ref" test_value = "X234567890123456789012345" changes = {field: test_value} - _, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + _, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) def test_put_write_concern_fail(self): # should get a 500 since there's no replicaset on the mongod instance - self.domain['contacts']['mongo_write_concern'] = {'w': 2} + self.domain["contacts"]["mongo_write_concern"] = {"w": 2} field = "ref" test_value = "X234567890123456789012345" changes = {field: test_value} - _, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + _, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert500(status) def test_put_string(self): @@ -172,68 +187,75 @@ def test_put_with_post_override(self): field = "ref" test_value = "1234567890123456789012345" changes = {field: test_value} - headers = [('X-HTTP-Method-Override', 'PUT'), - ('If-Match', self.item_etag), - ('Content-Type', 'application/x-www-form-urlencoded')] - r = self.test_client.post(self.item_id_url, data=changes, - headers=headers) + headers = [ + ("X-HTTP-Method-Override", "PUT"), + ("If-Match", self.item_etag), + ("Content-Type", "application/x-www-form-urlencoded"), + ] + r = self.test_client.post(self.item_id_url, data=changes, headers=headers) self.assert200(r.status_code) self.assertPutResponse(json.loads(r.get_data()), self.item_id) def test_put_default_value(self): - test_field = 'title' + test_field = "title" test_value = "Mr." - data = {'ref': '9234567890123456789054321'} + data = {"ref": "9234567890123456789054321"} r = self.perform_put(data) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(test_value, db_value) def test_put_readonly_value_same(self): - data = {'ref': self.item['ref'], - 'read_only_field': self.item['read_only_field']} - r, status = self.put(self.item_id_url, - data=data, - headers=[('If-Match', self.item_etag)]) + data = { + "ref": self.item["ref"], + "read_only_field": self.item["read_only_field"], + } + r, status = self.put( + self.item_id_url, data=data, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) def test_put_readonly_value_different(self): - field = 'read_only_field' - data = {'ref': self.item['ref'], field: 'somethingelse'} - r, status = self.put(self.item_id_url, - data=data, - headers=[('If-Match', self.item_etag)]) + field = "read_only_field" + data = {"ref": self.item["ref"], field: "somethingelse"} + r, status = self.put( + self.item_id_url, data=data, headers=[("If-Match", self.item_etag)] + ) self.assert422(status) self.assertValidationError(r, {field: "field is read-only"}) def test_put_subresource(self): _db = self.connection[MONGO_DBNAME] - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False # create random contact fake_contact = self.random_contacts(1)[0] fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id # update first invoice to reference the new contact - _db.invoices.update_one({'_id': ObjectId(self.invoice_id)}, - {'$set': {'person': fake_contact_id}}) + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id)) + response, status = self.get( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id) + ) etag = response[ETAG] data = {"inv_number": "new_number"} - headers = [('If-Match', etag)] - response, status = self.put('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id), - data=data, headers=headers) + headers = [("If-Match", etag)] + response, status = self.put( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id), + data=data, + headers=headers, + ) self.assert200(status) - self.assertPutResponse(response, self.invoice_id, 'peopleinvoices') - self.assertEqual(response.get('person'), str(fake_contact_id)) + self.assertPutResponse(response, self.invoice_id, "peopleinvoices") + self.assertEqual(response.get("person"), str(fake_contact_id)) def test_put_dbref_subresource(self): _db = self.connection[MONGO_DBNAME] - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False # create random contact fake_contact = self.random_contacts(1)[0] @@ -241,52 +263,57 @@ def test_put_dbref_subresource(self): # update first invoice to reference the new contact _db.invoices.update_one( - {'_id': ObjectId(self.invoice_id)}, - {'$set': { - 'person': fake_contact_id, - 'persondbref': DBRef("contacts", - ObjectId(fake_contact_id))}}) + {"_id": ObjectId(self.invoice_id)}, + { + "$set": { + "person": fake_contact_id, + "persondbref": DBRef("contacts", ObjectId(fake_contact_id)), + } + }, + ) # GET all invoices by new contact - response, status = self.get('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id)) + response, status = self.get( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id) + ) - self.assertEqual(response.get('persondbref')['$id'], - str(fake_contact_id)) + self.assertEqual(response.get("persondbref")["$id"], str(fake_contact_id)) etag = response[ETAG] data = {"inv_number": "new_number"} - headers = [('If-Match', etag)] - response, status = self.put('users/%s/invoices/%s' % - (fake_contact_id, self.invoice_id), - data=data, headers=headers) + headers = [("If-Match", etag)] + response, status = self.put( + "users/%s/invoices/%s" % (fake_contact_id, self.invoice_id), + data=data, + headers=headers, + ) self.assert200(status) - self.assertPutResponse(response, self.invoice_id, 'peopleinvoices') + self.assertPutResponse(response, self.invoice_id, "peopleinvoices") def test_put_bandwidth_saver(self): - changes = {'ref': '1234567890123456789012345'} + changes = {"ref": "1234567890123456789012345"} # bandwidth_saver is on by default - self.assertTrue(self.app.config['BANDWIDTH_SAVER']) + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) r = self.perform_put(changes) - self.assertFalse('ref' in r) - db_value = self.compare_put_with_get(self.app.config['ETAG'], r) - self.assertEqual(db_value, r[self.app.config['ETAG']]) - self.item_etag = r[self.app.config['ETAG']] + self.assertFalse("ref" in r) + db_value = self.compare_put_with_get(self.app.config["ETAG"], r) + self.assertEqual(db_value, r[self.app.config["ETAG"]]) + self.item_etag = r[self.app.config["ETAG"]] # test return all fields (bandwidth_saver off) - self.app.config['BANDWIDTH_SAVER'] = False + self.app.config["BANDWIDTH_SAVER"] = False r = self.perform_put(changes) - self.assertTrue('ref' in r) - db_value = self.compare_put_with_get(self.app.config['ETAG'], r) - self.assertEqual(db_value, r[self.app.config['ETAG']]) + self.assertTrue("ref" in r) + db_value = self.compare_put_with_get(self.app.config["ETAG"], r) + self.assertEqual(db_value, r[self.app.config["ETAG"]]) def test_put_dependency_fields_with_default(self): # Test that if a dependency is missing but has a default value then the # field is still accepted. See #353. - del(self.domain['contacts']['schema']['ref']['required']) + del (self.domain["contacts"]["schema"]["ref"]["required"]) field = "dependency_field2" test_value = "a value" changes = {field: test_value} @@ -296,44 +323,54 @@ def test_put_dependency_fields_with_default(self): def test_put_dependency_fields_with_wrong_value(self): # Test that if a dependency is not met, the put is refused - del(self.domain['contacts']['schema']['ref']['required']) - r, status = self.put(self.item_id_url, - data={'dependency_field3': 'value'}, - headers=[('If-Match', self.item_etag)]) + del (self.domain["contacts"]["schema"]["ref"]["required"]) + r, status = self.put( + self.item_id_url, + data={"dependency_field3": "value"}, + headers=[("If-Match", self.item_etag)], + ) self.assert422(status) - r, status = self.put(self.item_id_url, - data={'dependency_field1': 'value', - 'dependency_field3': 'value'}, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, + data={"dependency_field1": "value", "dependency_field3": "value"}, + headers=[("If-Match", self.item_etag)], + ) self.assert200(status) def test_put_custom_idfield(self): - product = {'title': 'Awesome Hypercube'} - r, status = self.put('products/FOOBAR', data=product) + product = {"title": "Awesome Hypercube"} + r, status = self.put("products/FOOBAR", data=product) self.assert201(status) def test_put_internal(self): # test that put_internal is available and working properly. - test_field = 'ref' + test_field = "ref" test_value = "9876543210987654321098765" data = {test_field: test_value} with self.app.test_request_context(self.item_id_url): r, _, _, status = put_internal( - self.known_resource, data, concurrency_check=False, - **{'_id': self.item_id}) + self.known_resource, + data, + concurrency_check=False, + **{"_id": self.item_id} + ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) self.assert200(status) def test_put_internal_skip_validation(self): # test that put_internal is available and working properly. - test_field = 'ref' + test_field = "ref" test_value = "9876543210987654321098765" data = {test_field: test_value} with self.app.test_request_context(self.item_id_url): r, _, _, status = put_internal( - self.known_resource, data, concurrency_check=False, - skip_validation=True, **{'_id': self.item_id}) + self.known_resource, + data, + concurrency_check=False, + skip_validation=True, + **{"_id": self.item_id} + ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) self.assert200(status) @@ -341,47 +378,43 @@ def test_put_internal_skip_validation(self): def test_put_etag_header(self): # test that Etag is always includer with response header. See #562. changes = {"ref": "1234567890123456789012345"} - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] - r = self.test_client.put(self.item_id_url, - data=json.dumps(changes), - headers=headers) - self.assertTrue('Etag' in r.headers) + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] + r = self.test_client.put( + self.item_id_url, data=json.dumps(changes), headers=headers + ) + self.assertTrue("Etag" in r.headers) # test that ETag is compliant to RFC 7232-2.3 and #794 is fixed. - etag = r.headers['ETag'] + etag = r.headers["ETag"] self.assertTrue(etag[0] == '"') self.assertTrue(etag[-1] == '"') def test_put_etag_header_enforce_ifmatch_disabled(self): - self.app.config['ENFORCE_IF_MATCH'] = False - changes = {'ref': '1234567890123456789012345'} - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] + self.app.config["ENFORCE_IF_MATCH"] = False + changes = {"ref": "1234567890123456789012345"} + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] r, status = self.put( - self.item_id_url, - data=json.dumps(changes), - headers=headers + self.item_id_url, data=json.dumps(changes), headers=headers ) self.assertTrue(ETAG in r) self.assertTrue(self.item_etag != r[ETAG]) def test_put_nested(self): changes = { - 'ref': '1234567890123456789012345', - 'location.city': 'a nested city', - 'location.address': 'a nested address' + "ref": "1234567890123456789012345", + "location.city": "a nested city", + "location.address": "a nested address", } r = self.perform_put(changes) - values = self.compare_put_with_get('location', r) - self.assertEqual(values['city'], 'a nested city') - self.assertEqual(values['address'], 'a nested address') + values = self.compare_put_with_get("location", r) + self.assertEqual(values["city"], "a nested city") + self.assertEqual(values["address"], "a nested address") def test_put_creates_unexisting_document(self): id = str(ObjectId()) - url = '%s/%s' % (self.known_resource_url, id) - id_field = self.domain[self.known_resource]['id_field'] + url = "%s/%s" % (self.known_resource_url, id) + id_field = self.domain[self.known_resource]["id_field"] changes = {"ref": "1234567890123456789012345"} r, status = self.put(url, data=changes) # 201 is a creation (POST) response @@ -390,19 +423,18 @@ def test_put_creates_unexisting_document(self): self.assertEqual(r[id_field], str(id)) def test_put_returns_404_on_unexisting_document(self): - self.app.config['UPSERT_ON_PUT'] = False + self.app.config["UPSERT_ON_PUT"] = False id = str(ObjectId()) - url = '%s/%s' % (self.known_resource_url, id) + url = "%s/%s" % (self.known_resource_url, id) changes = {"ref": "1234567890123456789012345"} r, status = self.put(url, data=changes) self.assert404(status) def test_put_creates_unexisting_document_with_url_as_id(self): id = str(ObjectId()) - url = '%s/%s' % (self.known_resource_url, id) - id_field = self.domain[self.known_resource]['id_field'] - changes = {"ref": "1234567890123456789012345", - id_field: str(ObjectId())} + url = "%s/%s" % (self.known_resource_url, id) + id_field = self.domain[self.known_resource]["id_field"] + changes = {"ref": "1234567890123456789012345", id_field: str(ObjectId())} r, status = self.put(url, data=changes) # 201 is a creation (POST) response self.assert201(status) @@ -412,34 +444,35 @@ def test_put_creates_unexisting_document_with_url_as_id(self): def test_put_creates_unexisting_document_fails_on_mismatching_id(self): id = str(ObjectId()) - id_field = self.domain[self.known_resource]['id_field'] + id_field = self.domain[self.known_resource]["id_field"] changes = {"ref": "1234567890123456789012345", id_field: id} - r, status = self.put(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert400(status) - self.assertTrue('immutable' in r['_error']['message']) + self.assertTrue("immutable" in r["_error"]["message"]) def test_put_type_coercion(self): - schema = self.domain[self.known_resource]['schema'] - schema['aninteger']['coerce'] = lambda string: int(float(string)) - changes = {'ref': '1234567890123456789054321', 'aninteger': '42.3'} - r, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + schema = self.domain[self.known_resource]["schema"] + schema["aninteger"]["coerce"] = lambda string: int(float(string)) + changes = {"ref": "1234567890123456789054321", "aninteger": "42.3"} + r, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) - r, status = self.get(r['_links']['self']['href']) - self.assertEqual(r['aninteger'], 42) + r, status = self.get(r["_links"]["self"]["href"]) + self.assertEqual(r["aninteger"], 42) def perform_put(self, changes): - r, status = self.put(self.item_id_url, - data=changes, - headers=[('If-Match', self.item_etag)]) + r, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assert200(status) self.assertPutResponse(r, self.item_id) return r def assertPutResponse(self, response, item_id, resource=None): - id_field = self.domain[resource or self.known_resource]['id_field'] + id_field = self.domain[resource or self.known_resource]["id_field"] self.assertTrue(STATUS in response) self.assertTrue(STATUS_OK in response[STATUS]) self.assertFalse(ISSUES in response) @@ -447,15 +480,14 @@ def assertPutResponse(self, response, item_id, resource=None): self.assertEqual(response[id_field], item_id) self.assertTrue(LAST_UPDATED in response) self.assertTrue(ETAG in response) - self.assertTrue('_links' in response) - self.assertItemLink(response['_links'], item_id) + self.assertTrue("_links" in response) + self.assertItemLink(response["_links"], item_id) def compare_put_with_get(self, fields, put_response): raw_r = self.test_client.get(self.item_id_url) r, status = self.parse_response(raw_r) self.assert200(status) - self.assertEqual(raw_r.headers.get('ETag').replace('"', ''), - put_response[ETAG]) + self.assertEqual(raw_r.headers.get("ETag").replace('"', ""), put_response[ETAG]) if isinstance(fields, str): return r[fields] else: @@ -481,6 +513,7 @@ def test_on_pre_PUT_contacts(self): def test_on_pre_PUT_dynamic_filter(self): def filter_this(resource, request, lookup): lookup["_id"] = self.unknown_item_id + self.app.on_pre_PUT += filter_this # Would normally delete the known document; will return 404 instead. r, s = self.parse_response(self.put()) @@ -506,14 +539,14 @@ def test_on_replace(self): self.app.on_replace += devent self.put() self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.new_ref, devent.called[1]['ref']) + self.assertEqual(self.new_ref, devent.called[1]["ref"]) self.assertEqual(3, len(devent.called)) def test_on_replace_contacts(self): devent = DummyEvent(self.before_replace) self.app.on_replace_contacts += devent self.put() - self.assertEqual(self.new_ref, devent.called[0]['ref']) + self.assertEqual(self.new_ref, devent.called[0]["ref"]) self.assertEqual(2, len(devent.called)) def test_on_replaced(self): @@ -521,27 +554,25 @@ def test_on_replaced(self): self.app.on_replaced += devent self.put() self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.new_ref, devent.called[1]['ref']) + self.assertEqual(self.new_ref, devent.called[1]["ref"]) self.assertEqual(3, len(devent.called)) def test_on_replaced_contacts(self): devent = DummyEvent(self.after_replace) self.app.on_replaced_contacts += devent self.put() - self.assertEqual(self.new_ref, devent.called[0]['ref']) + self.assertEqual(self.new_ref, devent.called[0]["ref"]) self.assertEqual(2, len(devent.called)) def before_replace(self): db = self.connection[MONGO_DBNAME] contact = db.contacts.find_one(ObjectId(self.item_id)) - return contact['ref'] == self.item_name + return contact["ref"] == self.item_name def after_replace(self): return not self.before_replace() def put(self): - headers = [('Content-Type', 'application/json'), - ('If-Match', self.item_etag)] + headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] data = json.dumps({"ref": self.new_ref}) - return self.test_client.put(self.item_id_url, data=data, - headers=headers) + return self.test_client.put(self.item_id_url, data=data, headers=headers) diff --git a/eve/tests/methods/ratelimit.py b/eve/tests/methods/ratelimit.py index 3b7c62753..dfabedeca 100644 --- a/eve/tests/methods/ratelimit.py +++ b/eve/tests/methods/ratelimit.py @@ -7,6 +7,7 @@ def setUp(self): super(TestRateLimit, self).setUp() try: from redis import Redis, ConnectionError + self.app.redis = Redis() try: self.app.redis.flushdb() @@ -16,10 +17,10 @@ def setUp(self): self.app.redis = None if self.app.redis: - self.app.config['RATE_LIMIT_GET'] = (1, 1) + self.app.config["RATE_LIMIT_GET"] = (1, 1) def test_ratelimit_home(self): - self.get_ratelimit("/") + self.get_ratelimit("/") def test_ratelimit_resource(self): self.get_ratelimit(self.known_resource_url) @@ -28,21 +29,21 @@ def test_ratelimit_item(self): self.get_ratelimit(self.item_id_url) def test_noratelimits(self): - self.app.config['RATE_LIMIT_GET'] = None + self.app.config["RATE_LIMIT_GET"] = None if self.app.redis: self.app.redis.flushdb() r = self.test_client.get("/") self.assert200(r.status_code) - self.assertTrue('X-RateLimit-Remaining' not in r.headers) - self.assertTrue('X-RateLimit-Limit' not in r.headers) - self.assertTrue('X-RateLimit-Reset' not in r.headers) + self.assertTrue("X-RateLimit-Remaining" not in r.headers) + self.assertTrue("X-RateLimit-Limit" not in r.headers) + self.assertTrue("X-RateLimit-Reset" not in r.headers) def get_ratelimit(self, url): if self.app.redis: # we want the following two GET to be executed within the same # tick (1 second) t1, t2 = 1, 2 - while (t1 != t2): + while t1 != t2: t1 = int(time.time()) r1 = self.test_client.get(url) t2 = int(time.time()) @@ -51,18 +52,17 @@ def get_ratelimit(self, url): time.sleep(1) self.assertRateLimit(r1) self.assertEqual(r2.status_code, 429) - self.assertTrue(b'Rate limit exceeded' in r2.get_data()) + self.assertTrue(b"Rate limit exceeded" in r2.get_data()) time.sleep(1) self.assertRateLimit(self.test_client.get(url)) else: - print("Skipped. Needs a running redis-server and 'pip install " - "redis'") + print("Skipped. Needs a running redis-server and 'pip install " "redis'") def assertRateLimit(self, r): - self.assertTrue('X-RateLimit-Remaining' in r.headers) - self.assertEqual(r.headers['X-RateLimit-Remaining'], '0') - self.assertTrue('X-RateLimit-Limit' in r.headers) - self.assertEqual(r.headers['X-RateLimit-Limit'], '1') + self.assertTrue("X-RateLimit-Remaining" in r.headers) + self.assertEqual(r.headers["X-RateLimit-Remaining"], "0") + self.assertTrue("X-RateLimit-Limit" in r.headers) + self.assertEqual(r.headers["X-RateLimit-Limit"], "1") # renouncing on testing the actual Reset value: - self.assertTrue('X-RateLimit-Reset' in r.headers) + self.assertTrue("X-RateLimit-Reset" in r.headers) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index f24568f64..5b1846d37 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -7,23 +7,24 @@ class TestRenders(TestBase): - def test_default_render(self): - r = self.test_client.get('/') - self.assertEqual(r.content_type, 'application/json') + r = self.test_client.get("/") + self.assertEqual(r.content_type, "application/json") def test_json_render(self): - r = self.test_client.get('/', headers=[('Accept', 'application/json')]) - self.assertEqual(r.content_type, 'application/json') + r = self.test_client.get("/", headers=[("Accept", "application/json")]) + self.assertEqual(r.content_type, "application/json") def test_xml_render(self): - r = self.test_client.get('/', headers=[('Accept', 'application/xml')]) - self.assertTrue('application/xml' in r.content_type) + r = self.test_client.get("/", headers=[("Accept", "application/xml")]) + self.assertTrue("application/xml" in r.content_type) def test_xml_url_escaping(self): - r = self.test_client.get('%s?max_results=1' % self.known_resource_url, - headers=[('Accept', 'application/xml')]) - self.assertTrue(b'&' in r.get_data()) + r = self.test_client.get( + "%s?max_results=1" % self.known_resource_url, + headers=[("Accept", "application/xml")], + ) + self.assertTrue(b"&" in r.get_data()) def test_xml_leaf_escaping(self): # test that even xml leaves content is being properly escaped @@ -31,311 +32,310 @@ def test_xml_leaf_escaping(self): # We need to assign a `person` to our test invoice _db = self.connection[MONGO_DBNAME] fake_contact = self.random_contacts(1)[0] - fake_contact['ref'] = "12345 & 67890" + fake_contact["ref"] = "12345 & 67890" fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id - r = self.test_client.get('%s/%s' % - (self.known_resource_url, fake_contact_id), - headers=[('Accept', 'application/xml')]) - self.assertTrue(b'12345 & 6789' in r.get_data()) + r = self.test_client.get( + "%s/%s" % (self.known_resource_url, fake_contact_id), + headers=[("Accept", "application/xml")], + ) + self.assertTrue(b"12345 & 6789" in r.get_data()) def test_xml_ordered_nodes(self): """ Test that xml nodes are ordered and #441 is addressed. """ - r = self.test_client.get('%s?max_results=1' % self.known_resource_url, - headers=[('Accept', 'application/xml')]) + r = self.test_client.get( + "%s?max_results=1" % self.known_resource_url, + headers=[("Accept", "application/xml")], + ) data = r.get_data() - idx1 = data.index(b'_created') - idx2 = data.index(b'_etag') - idx3 = data.index(b'_id') - idx4 = data.index(b'_updated') + idx1 = data.index(b"_created") + idx2 = data.index(b"_etag") + idx3 = data.index(b"_id") + idx4 = data.index(b"_updated") self.assertTrue(idx1 < idx2 < idx3 < idx4) - idx1 = data.index(b'max_results') - idx2 = data.index(b'page') - idx3 = data.index(b'total') + idx1 = data.index(b"max_results") + idx2 = data.index(b"page") + idx3 = data.index(b"total") self.assertTrue(idx1 < idx2 < idx3) - idx1 = data.index(b'last') - idx2 = data.index(b'next') - idx3 = data.index(b'parent') + idx1 = data.index(b"last") + idx2 = data.index(b"next") + idx3 = data.index(b"parent") self.assertTrue(idx1 < idx2 < idx3) def test_unknown_render(self): - r = self.test_client.get('/', headers=[('Accept', 'application/html')]) - self.assertEqual(r.content_type, 'application/json') + r = self.test_client.get("/", headers=[("Accept", "application/html")]) + self.assertEqual(r.content_type, "application/json") def test_json_xml_disabled(self): - self.app.config['RENDERERS'] = tuple() - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/json')]) + self.app.config["RENDERERS"] = tuple() + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/json")] + ) self.assert500(r.status_code) - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/xml')]) + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/xml")] + ) self.assert500(r.status_code) r = self.test_client.get(self.known_resource_url) self.assert500(r.status_code) def test_json_disabled(self): - self.app.config['RENDERERS'] = ('eve.render.XMLRenderer',) - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/json')]) - self.assertTrue('application/xml' in r.content_type) - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/xml')]) - self.assertTrue('application/xml' in r.content_type) + self.app.config["RENDERERS"] = ("eve.render.XMLRenderer",) + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/json")] + ) + self.assertTrue("application/xml" in r.content_type) + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/xml")] + ) + self.assertTrue("application/xml" in r.content_type) r = self.test_client.get(self.known_resource_url) - self.assertTrue('application/xml' in r.content_type) + self.assertTrue("application/xml" in r.content_type) def test_xml_disabled(self): - self.app.config['RENDERERS'] = ('eve.render.JSONRenderer',) - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/xml')]) - self.assertEqual(r.content_type, 'application/json') - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/json')]) - self.assertEqual(r.content_type, 'application/json') + self.app.config["RENDERERS"] = ("eve.render.JSONRenderer",) + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/xml")] + ) + self.assertEqual(r.content_type, "application/json") + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/json")] + ) + self.assertEqual(r.content_type, "application/json") r = self.test_client.get(self.known_resource_url) - self.assertEqual(r.content_type, 'application/json') + self.assertEqual(r.content_type, "application/json") def test_json_keys_sorted(self): - self.app.config['JSON_SORT_KEYS'] = True - r = self.test_client.get(self.known_resource_url, - headers=[('Accept', 'application/json')]) + self.app.config["JSON_SORT_KEYS"] = True + r = self.test_client.get( + self.known_resource_url, headers=[("Accept", "application/json")] + ) self.assertEqual( - json.dumps(json.loads(r.get_data()), sort_keys=True).encode(), - r.get_data() + json.dumps(json.loads(r.get_data()), sort_keys=True).encode(), r.get_data() ) def test_jsonp_enabled(self): arg = "callback" - self.app.config['JSONP_ARGUMENT'] = arg + self.app.config["JSONP_ARGUMENT"] = arg val = "JSON_CALLBACK" - r = self.test_client.get('/?%s=%s' % (arg, val)) - self.assertTrue(r.get_data().decode('utf-8').startswith(val)) + r = self.test_client.get("/?%s=%s" % (arg, val)) + self.assertTrue(r.get_data().decode("utf-8").startswith(val)) def test_CORS(self): # no CORS headers if Origin is not provided with the request. - r = self.test_client.get('/') - self.assertFalse('Access-Control-Allow-Origin' in r.headers) - self.assertFalse('Access-Control-Allow-Methods' in r.headers) - self.assertFalse('Access-Control-Max-Age' in r.headers) - self.assertFalse('Access-Control-Expose-Headers' in r.headers) - self.assertFalse('Access-Control-Allow-Credentials' in r.headers) + r = self.test_client.get("/") + self.assertFalse("Access-Control-Allow-Origin" in r.headers) + self.assertFalse("Access-Control-Allow-Methods" in r.headers) + self.assertFalse("Access-Control-Max-Age" in r.headers) + self.assertFalse("Access-Control-Expose-Headers" in r.headers) + self.assertFalse("Access-Control-Allow-Credentials" in r.headers) self.assert200(r.status_code) # test that if X_DOMAINS is set to '*', then any Origin value is # allowed. Also test that only the Origin header included with the # request will be returned to the client. - self.app.config['X_DOMAINS'] = '*' - r = self.test_client.get('/', headers=[('Origin', - 'http://example.com')]) + self.app.config["X_DOMAINS"] = "*" + r = self.test_client.get("/", headers=[("Origin", "http://example.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://example.com') - self.assertEqual(r.headers['Vary'], 'Origin') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://example.com") + self.assertEqual(r.headers["Vary"], "Origin") # Given that CORS is activated with X_DOMAINS = '*', # test that if X_ALLOW_CREDENTIALS is set to True # then the relevant header is included in the response - self.app.config['X_ALLOW_CREDENTIALS'] = True - r = self.test_client.get('/', headers=[('Origin', - 'http://example.com')]) + self.app.config["X_ALLOW_CREDENTIALS"] = True + r = self.test_client.get("/", headers=[("Origin", "http://example.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Credentials'], 'true') + self.assertEqual(r.headers["Access-Control-Allow-Credentials"], "true") # with any other non-True value, it is missing - self.app.config['X_ALLOW_CREDENTIALS'] = False - r = self.test_client.get('/', headers=[('Origin', - 'http://example.com')]) + self.app.config["X_ALLOW_CREDENTIALS"] = False + r = self.test_client.get("/", headers=[("Origin", "http://example.com")]) self.assert200(r.status_code) - self.assertFalse('Access-Control-Allow-Credentials' in r.headers) + self.assertFalse("Access-Control-Allow-Credentials" in r.headers) # test that if a list is set for X_DOMAINS, then: # 1. only list values are accepted; # 2. only the value included with the request is returned back. - self.app.config['X_DOMAINS'] = ['http://1of2.com', 'http://2of2.com'] - r = self.test_client.get('/', headers=[('Origin', 'http://1of2.com')]) + self.app.config["X_DOMAINS"] = ["http://1of2.com", "http://2of2.com"] + r = self.test_client.get("/", headers=[("Origin", "http://1of2.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://1of2.com') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://1of2.com") - r = self.test_client.get('/', headers=[('Origin', 'http://2of2.com')]) + r = self.test_client.get("/", headers=[("Origin", "http://2of2.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://2of2.com') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://2of2.com") - r = self.test_client.get('/', headers=[('Origin', - 'http://notreally.com')]) + r = self.test_client.get("/", headers=[("Origin", "http://notreally.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") # other Access-Control-Allow- headers are included. - self.assertTrue('Access-Control-Allow-Headers' in r.headers) - self.assertTrue('Access-Control-Allow-Methods' in r.headers) - self.assertTrue('Access-Control-Max-Age' in r.headers) - self.assertTrue('Access-Control-Expose-Headers' in r.headers) + self.assertTrue("Access-Control-Allow-Headers" in r.headers) + self.assertTrue("Access-Control-Allow-Methods" in r.headers) + self.assertTrue("Access-Control-Max-Age" in r.headers) + self.assertTrue("Access-Control-Expose-Headers" in r.headers) # unescaped dots of old (pre v0.7) or malicious X_DOMAINS definitions # would be interpreted as any character, causing security issue with # bad guy registering wwwxgithub.com to pass as www.github.com (see # #660). - self.app.config['X_DOMAINS'] = ['http://www.github.com'] - r = self.test_client.get('/', headers=[('Origin', - 'http://wwwxgithub.com')]) + self.app.config["X_DOMAINS"] = ["http://www.github.com"] + r = self.test_client.get("/", headers=[("Origin", "http://wwwxgithub.com")]) self.assert200(r.status_code) - self.assertFalse('http://wwwxgithub.com' in - r.headers['Access-Control-Allow-Origin']) + self.assertFalse( + "http://wwwxgithub.com" in r.headers["Access-Control-Allow-Origin"] + ) # test that X_DOMAINS does not match # if the origin contains extra characters (#974) - r = self.test_client.get('/', headers=[('Origin', - 'http://1of2.com:8000')]) + r = self.test_client.get("/", headers=[("Origin", "http://1of2.com:8000")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") def test_CORS_regex(self): # test if X_DOMAINS_RE is set with a list of regexes, # origins are matched against this list (#974) - self.app.config['X_DOMAINS_RE'] = ['^http://sub-\d{3}\.domain\.com$'] + self.app.config["X_DOMAINS_RE"] = ["^http://sub-\d{3}\.domain\.com$"] - r = self.test_client.get('/', headers=[('Origin', - 'http://sub-123.domain.com')]) + r = self.test_client.get("/", headers=[("Origin", "http://sub-123.domain.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://sub-123.domain.com') + self.assertEqual( + r.headers["Access-Control-Allow-Origin"], "http://sub-123.domain.com" + ) # test that similar domains are not allowed - r = self.test_client.get('/', headers=[('Origin', - 'http://sub-1234.domain.com')]) + r = self.test_client.get( + "/", headers=[("Origin", "http://sub-1234.domain.com")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") r = self.test_client.get( - '/', headers=[('Origin', 'http://sub-123.domain.com:8000')]) + "/", headers=[("Origin", "http://sub-123.domain.com:8000")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") - r = self.test_client.get('/', headers=[('Origin', - 'http://sub-123xdomain.com')]) + r = self.test_client.get("/", headers=[("Origin", "http://sub-123xdomain.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") # test that invalid regexes are ignored, especially '*' - self.app.config['X_DOMAINS_RE'] = ['*'] - r = self.test_client.get('/', headers=[('Origin', - 'http://www.example.com')]) + self.app.config["X_DOMAINS_RE"] = ["*"] + r = self.test_client.get("/", headers=[("Origin", "http://www.example.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") def test_CORS_MAX_AGE(self): - self.app.config['X_DOMAINS'] = '*' - r = self.test_client.get('/', headers=[('Origin', - 'http://example.com')]) - self.assertEqual(r.headers['Access-Control-Max-Age'], - '21600') - - self.app.config['X_MAX_AGE'] = 2000 - r = self.test_client.get('/', headers=[('Origin', - 'http://example.com')]) - self.assertEqual(r.headers['Access-Control-Max-Age'], - '2000') - - def test_CORS_OPTIONS(self, url='/', methods=None): + self.app.config["X_DOMAINS"] = "*" + r = self.test_client.get("/", headers=[("Origin", "http://example.com")]) + self.assertEqual(r.headers["Access-Control-Max-Age"], "21600") + + self.app.config["X_MAX_AGE"] = 2000 + r = self.test_client.get("/", headers=[("Origin", "http://example.com")]) + self.assertEqual(r.headers["Access-Control-Max-Age"], "2000") + + def test_CORS_OPTIONS(self, url="/", methods=None): if methods is None: methods = [] - r = self.test_client.open(url, method='OPTIONS') - self.assertFalse('Access-Control-Allow-Origin' in r.headers) - self.assertFalse('Access-Control-Allow-Methods' in r.headers) - self.assertFalse('Access-Control-Max-Age' in r.headers) - self.assertFalse('Access-Control-Expose-Headers' in r.headers) - self.assertFalse('Access-Control-Allow-Credentials' in r.headers) + r = self.test_client.open(url, method="OPTIONS") + self.assertFalse("Access-Control-Allow-Origin" in r.headers) + self.assertFalse("Access-Control-Allow-Methods" in r.headers) + self.assertFalse("Access-Control-Max-Age" in r.headers) + self.assertFalse("Access-Control-Expose-Headers" in r.headers) + self.assertFalse("Access-Control-Allow-Credentials" in r.headers) self.assert200(r.status_code) # test that if X_DOMAINS is set to '*', then any Origin value is # allowed. Also test that only the Origin header included with the # request will be # returned back to the client. - self.app.config['X_DOMAINS'] = '*' - r = self.test_client.open(url, method='OPTIONS', - headers=[('Origin', 'http://example.com')]) + self.app.config["X_DOMAINS"] = "*" + r = self.test_client.open( + url, method="OPTIONS", headers=[("Origin", "http://example.com")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://example.com') - self.assertEqual(r.headers['Vary'], 'Origin') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://example.com") + self.assertEqual(r.headers["Vary"], "Origin") for m in methods: - self.assertTrue(m in r.headers['Access-Control-Allow-Methods']) + self.assertTrue(m in r.headers["Access-Control-Allow-Methods"]) # Given that CORS is activated with X_DOMAINS = '*' # test that if X_ALLOW_CREDENTIALS is set to True # then the relevant header is included in the response - self.app.config['X_ALLOW_CREDENTIALS'] = True - r = self.test_client.open(url, method='OPTIONS', - headers=[('Origin', 'http://example.com')]) + self.app.config["X_ALLOW_CREDENTIALS"] = True + r = self.test_client.open( + url, method="OPTIONS", headers=[("Origin", "http://example.com")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Credentials'], 'true') + self.assertEqual(r.headers["Access-Control-Allow-Credentials"], "true") # with any other non-True value, it is missing - self.app.config['X_ALLOW_CREDENTIALS'] = False - r = self.test_client.open(url, method='OPTIONS', - headers=[('Origin', 'http://example.com')]) + self.app.config["X_ALLOW_CREDENTIALS"] = False + r = self.test_client.open( + url, method="OPTIONS", headers=[("Origin", "http://example.com")] + ) self.assert200(r.status_code) - self.assertFalse('Access-Control-Allow-Credentials' in r.headers) + self.assertFalse("Access-Control-Allow-Credentials" in r.headers) - self.app.config['X_DOMAINS'] = ['http://1of2.com', 'http://2of2.com'] - r = self.test_client.open(url, method='OPTIONS', - headers=[('Origin', 'http://1of2.com')]) + self.app.config["X_DOMAINS"] = ["http://1of2.com", "http://2of2.com"] + r = self.test_client.open( + url, method="OPTIONS", headers=[("Origin", "http://1of2.com")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://1of2.com') - r = self.test_client.open(url, method='OPTIONS', - headers=[('Origin', 'http://2of2.com')]) + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://1of2.com") + r = self.test_client.open( + url, method="OPTIONS", headers=[("Origin", "http://2of2.com")] + ) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], - 'http://2of2.com') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "http://2of2.com") for m in methods: - self.assertTrue(m in r.headers['Access-Control-Allow-Methods']) + self.assertTrue(m in r.headers["Access-Control-Allow-Methods"]) - self.assertTrue('Access-Control-Allow-Origin' in r.headers) - self.assertTrue('Access-Control-Max-Age' in r.headers) - self.assertTrue('Access-Control-Expose-Headers' in r.headers) + self.assertTrue("Access-Control-Allow-Origin" in r.headers) + self.assertTrue("Access-Control-Max-Age" in r.headers) + self.assertTrue("Access-Control-Expose-Headers" in r.headers) - r = self.test_client.get(url, headers=[('Origin', - 'http://not_an_example.com')]) + r = self.test_client.get(url, headers=[("Origin", "http://not_an_example.com")]) self.assert200(r.status_code) - self.assertEqual(r.headers['Access-Control-Allow-Origin'], '') + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "") for m in methods: - self.assertTrue(m in r.headers['Access-Control-Allow-Methods']) + self.assertTrue(m in r.headers["Access-Control-Allow-Methods"]) def test_CORS_OPTIONS_resources(self): - prefix = api_prefix(self.app.config['URL_PREFIX'], - self.app.config['API_VERSION']) - - del(self.domain['peopleinvoices']) - del(self.domain['peoplerequiredinvoices']) - del(self.domain['peoplesearches']) - del(self.domain['internal_transactions']) - del(self.domain['child_products']) - for _, settings in self.app.config['DOMAIN'].items(): + prefix = api_prefix( + self.app.config["URL_PREFIX"], self.app.config["API_VERSION"] + ) + + del (self.domain["peopleinvoices"]) + del (self.domain["peoplerequiredinvoices"]) + del (self.domain["peoplesearches"]) + del (self.domain["internal_transactions"]) + del (self.domain["child_products"]) + for _, settings in self.app.config["DOMAIN"].items(): # resource endpoint - url = '%s/%s/' % (prefix, settings['url']) - methods = settings['resource_methods'] + ['OPTIONS'] + url = "%s/%s/" % (prefix, settings["url"]) + methods = settings["resource_methods"] + ["OPTIONS"] self.test_CORS_OPTIONS(url, methods) def test_CORS_OPTIONS_item(self): - prefix = api_prefix(self.app.config['URL_PREFIX'], - self.app.config['API_VERSION']) + prefix = api_prefix( + self.app.config["URL_PREFIX"], self.app.config["API_VERSION"] + ) - url = '%s%s' % (prefix, self.item_id_url) - methods = (self.domain[self.known_resource]['resource_methods'] + - ['OPTIONS']) + url = "%s%s" % (prefix, self.item_id_url) + methods = self.domain[self.known_resource]["resource_methods"] + ["OPTIONS"] self.test_CORS_OPTIONS(url, methods) - url = '%s%s/%s' % (prefix, self.known_resource_url, self.item_ref) - methods = ['GET', 'OPTIONS'] + url = "%s%s/%s" % (prefix, self.known_resource_url, self.item_ref) + methods = ["GET", "OPTIONS"] def test_CORS_OPTIONS_schema(self): """ Test that CORS is also supported at SCHEMA_ENDPOINT """ - self.app.config['SCHEMA_ENDPOINT'] = 'schema' + self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() - methods = ['GET', 'OPTIONS'] - self.test_CORS_OPTIONS('schema', methods) + methods = ["GET", "OPTIONS"] + self.test_CORS_OPTIONS("schema", methods) diff --git a/eve/tests/response.py b/eve/tests/response.py index a4709b443..6ba1db3f5 100644 --- a/eve/tests/response.py +++ b/eve/tests/response.py @@ -8,17 +8,16 @@ class TestResponse(TestBase): - def setUp(self): super(TestResponse, self).setUp() - self.r = self.test_client.get('/%s/' % self.empty_resource) + self.r = self.test_client.get("/%s/" % self.empty_resource) def test_response_data(self): response = None try: response = literal_eval(self.r.get_data().decode()) except: - self.fail('standard response cannot be converted to a dict') + self.fail("standard response cannot be converted to a dict") self.assertTrue(isinstance(response, dict)) def test_response_object(self): @@ -26,84 +25,82 @@ def test_response_object(self): self.assertTrue(isinstance(response, dict)) self.assertEqual(len(response), 3) - resource = response.get('_items') + resource = response.get("_items") self.assertTrue(isinstance(resource, list)) - links = response.get('_links') + links = response.get("_links") self.assertTrue(isinstance(links, dict)) - meta = response.get('_meta') + meta = response.get("_meta") self.assertTrue(isinstance(meta, dict)) def test_response_pretty(self): # check if pretty printing was successful by checking the length of the # response since pretty printing the respone makes it longer and not # type dict anymore - self.r = self.test_client.get('/%s/?pretty' % self.empty_resource) + self.r = self.test_client.get("/%s/?pretty" % self.empty_resource) response = self.r.get_data().decode() self.assertEqual(len(response), 300) class TestNoHateoas(TestBase): - def setUp(self): super(TestNoHateoas, self).setUp() - self.app.config['HATEOAS'] = False - self.domain[self.known_resource]['hateoas'] = False + self.app.config["HATEOAS"] = False + self.domain[self.known_resource]["hateoas"] = False def test_get_no_hateoas_resource(self): r = self.test_client.get(self.known_resource_url) response = json.loads(r.get_data().decode()) self.assertTrue(isinstance(response, dict)) - self.assertEqual(len(response['_items']), 25) - item = response['_items'][0] + self.assertEqual(len(response["_items"]), 25) + item = response["_items"][0] self.assertTrue(isinstance(item, dict)) - self.assertTrue('_links' not in response) + self.assertTrue("_links" not in response) def test_get_no_hateoas_item(self): r = self.test_client.get(self.item_id_url) response = json.loads(r.get_data().decode()) self.assertTrue(isinstance(response, dict)) - self.assertTrue('_links' not in response) + self.assertTrue("_links" not in response) def test_get_no_hateoas_homepage(self): - r = self.test_client.get('/') + r = self.test_client.get("/") self.assert200(r.status_code) def test_get_no_hateoas_homepage_reply(self): - r = self.test_client.get('/') + r = self.test_client.get("/") resp = json.loads(r.get_data().decode()) self.assertEqual(resp, {}) - self.app.config['INFO'] = '_info' + self.app.config["INFO"] = "_info" - r = self.test_client.get('/') + r = self.test_client.get("/") resp = json.loads(r.get_data().decode()) - self.assertEqual(resp['_info']['server'], 'Eve') - self.assertEqual(resp['_info']['version'], eve.__version__) + self.assertEqual(resp["_info"]["server"], "Eve") + self.assertEqual(resp["_info"]["version"], eve.__version__) - settings_file = os.path.join(self.this_directory, 'test_version.py') + settings_file = os.path.join(self.this_directory, "test_version.py") self.app = eve.Eve(settings=settings_file) - self.app.config['INFO'] = '_info' + self.app.config["INFO"] = "_info" - r = self.app.test_client().get('/v1') + r = self.app.test_client().get("/v1") resp = json.loads(r.get_data().decode()) - self.assertEqual(resp['_info']['api_version'], - self.app.config['API_VERSION']) - self.assertEqual(resp['_info']['server'], 'Eve') - self.assertEqual(resp['_info']['version'], eve.__version__) + self.assertEqual(resp["_info"]["api_version"], self.app.config["API_VERSION"]) + self.assertEqual(resp["_info"]["server"], "Eve") + self.assertEqual(resp["_info"]["version"], eve.__version__) def test_post_no_hateoas(self): - data = {'item1': json.dumps({"ref": "1234567890123456789054321"})} - headers = [('Content-Type', 'application/x-www-form-urlencoded')] - r = self.test_client.post(self.known_resource_url, data=data, - headers=headers) + data = {"item1": json.dumps({"ref": "1234567890123456789054321"})} + headers = [("Content-Type", "application/x-www-form-urlencoded")] + r = self.test_client.post(self.known_resource_url, data=data, headers=headers) response = json.loads(r.get_data().decode()) - self.assertTrue('_links' not in response) + self.assertTrue("_links" not in response) def test_patch_no_hateoas(self): - data = {'item1': json.dumps({"ref": "0000000000000000000000000"})} - headers = [('Content-Type', 'application/x-www-form-urlencoded'), - ('If-Match', self.item_etag)] - r = self.test_client.patch(self.item_id_url, data=data, - headers=headers) + data = {"item1": json.dumps({"ref": "0000000000000000000000000"})} + headers = [ + ("Content-Type", "application/x-www-form-urlencoded"), + ("If-Match", self.item_etag), + ] + r = self.test_client.patch(self.item_id_url, data=data, headers=headers) response = json.loads(r.get_data().decode()) - self.assertTrue('_links' not in response) + self.assertTrue("_links" not in response) diff --git a/eve/tests/test_prefix.py b/eve/tests/test_prefix.py index baa0ed1f9..750e3c4cb 100644 --- a/eve/tests/test_prefix.py +++ b/eve/tests/test_prefix.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -RESOURCE_METHODS = ['GET', 'POST'] -URL_PREFIX = 'prefix' -DOMAIN = {'contacts': {}} +RESOURCE_METHODS = ["GET", "POST"] +URL_PREFIX = "prefix" +DOMAIN = {"contacts": {}} diff --git a/eve/tests/test_prefix_version.py b/eve/tests/test_prefix_version.py index 363397d6c..12142bd6b 100644 --- a/eve/tests/test_prefix_version.py +++ b/eve/tests/test_prefix_version.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -URL_PREFIX = 'prefix' -API_VERSION = 'v1' -DOMAIN = {'contacts': {}} +URL_PREFIX = "prefix" +API_VERSION = "v1" +DOMAIN = {"contacts": {}} diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 3f8a2a5ea..02e47dc59 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -2,369 +2,278 @@ import copy -MONGO_HOST = 'localhost' +MONGO_HOST = "localhost" MONGO_PORT = 27017 -MONGO_USERNAME = MONGO1_USERNAME = 'test_user' -MONGO_PASSWORD = MONGO1_PASSWORD = 'test_pw' -MONGO_DBNAME, MONGO1_DBNAME = 'eve_test', 'eve_test1' -ID_FIELD = '_id' +MONGO_USERNAME = MONGO1_USERNAME = "test_user" +MONGO_PASSWORD = MONGO1_PASSWORD = "test_pw" +MONGO_DBNAME, MONGO1_DBNAME = "eve_test", "eve_test1" +ID_FIELD = "_id" -RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] -ITEM_METHODS = ['GET', 'PATCH', 'DELETE', 'PUT'] -ITEM_CACHE_CONTROL = '' +RESOURCE_METHODS = ["GET", "POST", "DELETE"] +ITEM_METHODS = ["GET", "PATCH", "DELETE", "PUT"] +ITEM_CACHE_CONTROL = "" ITEM_LOOKUP = True ITEM_LOOKUP_FIELD = ID_FIELD disabled_bulk = { - 'url': 'somebulkurl', - 'item_title': 'bulkdisabled', - 'bulk_enabled': False, - 'schema': { - 'string_field': { - 'type': 'string' - } - } + "url": "somebulkurl", + "item_title": "bulkdisabled", + "bulk_enabled": False, + "schema": {"string_field": {"type": "string"}}, } contacts = { - 'url': 'arbitraryurl', - 'cache_control': 'max-age=20,must-revalidate', - 'cache_expires': 20, - 'item_title': 'contact', - 'additional_lookup': { - 'url': r'regex("[\w]+")', # to be unique field - 'field': 'ref' + "url": "arbitraryurl", + "cache_control": "max-age=20,must-revalidate", + "cache_expires": 20, + "item_title": "contact", + "additional_lookup": { + "url": r'regex("[\w]+")', # to be unique field + "field": "ref", }, - 'datasource': {'filter': {'username': {'$exists': False}}}, - 'schema': { - 'ref': { - 'type': 'string', - 'minlength': 25, - 'maxlength': 25, - 'required': True, - 'unique': True, - }, - 'media': { - 'type': 'media' - }, - 'prog': { - 'type': 'integer' - }, - 'role': { - 'type': 'list', - 'allowed': ["agent", "client", "vendor"], - }, - 'rows': { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - 'sku': {'type': 'string', 'maxlength': 10}, - 'price': {'type': 'integer'}, + "datasource": {"filter": {"username": {"$exists": False}}}, + "schema": { + "ref": { + "type": "string", + "minlength": 25, + "maxlength": 25, + "required": True, + "unique": True, + }, + "media": {"type": "media"}, + "prog": {"type": "integer"}, + "role": {"type": "list", "allowed": ["agent", "client", "vendor"]}, + "rows": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "sku": {"type": "string", "maxlength": 10}, + "price": {"type": "integer"}, }, }, }, - 'alist': { - 'type': 'list', - 'items': [{'type': 'string'}, {'type': 'integer'}, ] - }, - 'location': { - 'type': 'dict', - 'schema': { - 'address': {'type': 'string'}, - 'city': {'type': 'string', 'required': True} + "alist": {"type": "list", "items": [{"type": "string"}, {"type": "integer"}]}, + "location": { + "type": "dict", + "schema": { + "address": {"type": "string"}, + "city": {"type": "string", "required": True}, }, }, - 'born': { - 'type': 'datetime', - }, - 'tid': { - 'type': 'objectid', - 'nullable': True - }, - 'title': { - 'type': 'string', - 'default': 'Mr.', - }, - 'id_list': { - 'type': 'list', - 'schema': {'type': 'objectid'} - }, - 'id_list_of_dict': { - 'type': 'list', - 'schema': {'type': 'dict', 'schema': {'id': {'type': 'objectid'}}} - }, - 'id_list_fixed_len': { - 'type': 'list', - 'items': [{'type': 'objectid'}] - }, - 'dict_list_fixed_len': { - 'type': 'list', - 'items': [ - { - 'type': 'dict', - 'schema': {'key1': {'type': 'string'}} - }, - { - 'type': 'dict', - 'schema': {'key2': {'type': 'integer'}} + "born": {"type": "datetime"}, + "tid": {"type": "objectid", "nullable": True}, + "title": {"type": "string", "default": "Mr."}, + "id_list": {"type": "list", "schema": {"type": "objectid"}}, + "id_list_of_dict": { + "type": "list", + "schema": {"type": "dict", "schema": {"id": {"type": "objectid"}}}, + }, + "id_list_fixed_len": {"type": "list", "items": [{"type": "objectid"}]}, + "dict_list_fixed_len": { + "type": "list", + "items": [ + {"type": "dict", "schema": {"key1": {"type": "string"}}}, + {"type": "dict", "schema": {"key2": {"type": "integer"}}}, + ], + }, + "dependency_field1": {"type": "string", "default": "default"}, + "dependency_field2": {"type": "string", "dependencies": ["dependency_field1"]}, + "dependency_field3": { + "type": "string", + "dependencies": {"dependency_field1": "value"}, + }, + "read_only_field": {"type": "string", "default": "default", "readonly": True}, + "dict_with_read_only": { + "type": "dict", + "schema": { + "read_only_in_dict": { + "type": "string", + "default": "default", + "readonly": True, } - ] - }, - 'dependency_field1': { - 'type': 'string', - 'default': 'default' - }, - 'dependency_field2': { - 'type': 'string', - 'dependencies': ['dependency_field1'] - }, - 'dependency_field3': { - 'type': 'string', - 'dependencies': {'dependency_field1': 'value'} - }, - 'read_only_field': { - 'type': 'string', - 'default': 'default', - 'readonly': True - }, - 'dict_with_read_only': { - 'type': 'dict', - 'schema': { - 'read_only_in_dict': { - 'type': 'string', - 'default': 'default', - 'readonly': True - } - } - }, - 'key1': { - 'type': 'string', - }, - 'keyschema_dict': { - 'type': 'dict', - 'keyschema': {'type': 'string', 'regex': '[a-z]+'} - }, - 'valueschema_dict': { - 'type': 'dict', - 'valueschema': {'type': 'integer'} - }, - 'aninteger': { - 'type': 'integer', - }, - 'afloat': { - 'type': 'float', + }, }, - 'anumber': { - 'type': 'number' + "key1": {"type": "string"}, + "keyschema_dict": { + "type": "dict", + "keyschema": {"type": "string", "regex": "[a-z]+"}, + }, + "valueschema_dict": {"type": "dict", "valueschema": {"type": "integer"}}, + "aninteger": {"type": "integer"}, + "afloat": {"type": "float"}, + "anumber": {"type": "number"}, + "dict_valueschema": { + "type": "dict", + "valueschema": { + "type": "dict", + "schema": {"challenge": {"type": "objectid"}}, + }, }, - 'dict_valueschema': { - 'type': 'dict', - 'valueschema': { - 'type': 'dict', - 'schema': { - 'challenge': {'type': 'objectid'} - } - } - } - } + }, } users = copy.deepcopy(contacts) -users['url'] = 'users' -users['datasource'] = {'source': 'contacts', - 'filter': {'username': {'$exists': True}}, - 'projection': {'username': 1, 'ref': 1}} -users['schema']['username'] = {'type': 'string', 'required': True} -users['resource_methods'] = ['DELETE', 'POST', 'GET'] -users['item_title'] = 'user' -users['additional_lookup']['field'] = 'username' +users["url"] = "users" +users["datasource"] = { + "source": "contacts", + "filter": {"username": {"$exists": True}}, + "projection": {"username": 1, "ref": 1}, +} +users["schema"]["username"] = {"type": "string", "required": True} +users["resource_methods"] = ["DELETE", "POST", "GET"] +users["item_title"] = "user" +users["additional_lookup"]["field"] = "username" contacts_hide_born = copy.deepcopy(contacts) -contacts_hide_born['url'] = 'contacts/hide_born' -contacts_hide_born['datasource']['source'] = 'contacts' -contacts_hide_born['datasource']['projection'] = {'born': 0} +contacts_hide_born["url"] = "contacts/hide_born" +contacts_hide_born["datasource"]["source"] = "contacts" +contacts_hide_born["datasource"]["projection"] = {"born": 0} contacts_hide_media = copy.deepcopy(contacts) -contacts_hide_media['url'] = 'contacts/hide_media' -contacts_hide_media['datasource']['source'] = 'contacts' -contacts_hide_media['datasource']['projection'] = {'media': 0, 'born': 0} +contacts_hide_media["url"] = "contacts/hide_media" +contacts_hide_media["datasource"]["source"] = "contacts" +contacts_hide_media["datasource"]["projection"] = {"media": 0, "born": 0} invoices = { - 'schema': { - 'inv_number': {'type': 'string'}, - 'person': { - 'type': 'objectid', - 'data_relation': {'resource': 'contacts'} - }, - 'invoicing_contacts': { - 'type': 'list', - 'data_relation': {'resource': 'contacts'} - }, - 'persondbref': { - 'type': 'dbref', - 'data_relation': {'resource': 'contacts'} - }, - 'decimal_number': {'type': 'decimal'}, + "schema": { + "inv_number": {"type": "string"}, + "person": {"type": "objectid", "data_relation": {"resource": "contacts"}}, + "invoicing_contacts": { + "type": "list", + "data_relation": {"resource": "contacts"}, + }, + "persondbref": {"type": "dbref", "data_relation": {"resource": "contacts"}}, + "decimal_number": {"type": "decimal"}, } } # This resource is used to test app initialization when using resource # level versioning versioned_invoices = copy.deepcopy(invoices) -versioned_invoices['versioning'] = True +versioned_invoices["versioning"] = True # This resource is used to test subresources that have a reference/objectid # field that is set to be required. required_invoices = copy.deepcopy(invoices) -required_invoices['schema']['person']['required'] = True +required_invoices["schema"]["person"]["required"] = True companies = { - 'item_title': 'company', - 'schema': { - 'departments': { - 'type': 'list', - 'schema': { - 'type': 'dict', - 'schema': { - 'title': {'type': 'string'}, - 'members': { - 'type': 'list', - 'schema': { - 'type': 'objectid', - 'data_relation': {'resource': 'contacts'}, - } - } - } - } + "item_title": "company", + "schema": { + "departments": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "title": {"type": "string"}, + "members": { + "type": "list", + "schema": { + "type": "objectid", + "data_relation": {"resource": "contacts"}, + }, + }, + }, + }, }, - 'holding': { - 'type': 'objectid', - 'data_relation': {'resource': 'companies'}, - } - } + "holding": {"type": "objectid", "data_relation": {"resource": "companies"}}, + }, } users_overseas = copy.deepcopy(users) -users_overseas['url'] = 'users/overseas' -users_overseas['datasource'] = {'source': 'contacts'} +users_overseas["url"] = "users/overseas" +users_overseas["datasource"] = {"source": "contacts"} -payments = { - 'resource_methods': ['GET'], - 'item_methods': ['GET'], -} +payments = {"resource_methods": ["GET"], "item_methods": ["GET"]} empty = copy.deepcopy(invoices) user_restricted_access = copy.deepcopy(contacts) -user_restricted_access['url'] = 'restricted' -user_restricted_access['datasource'] = {'source': 'contacts'} +user_restricted_access["url"] = "restricted" +user_restricted_access["datasource"] = {"source": "contacts"} users_invoices = copy.deepcopy(invoices) -users_invoices['url'] = 'users//invoices' -users_invoices['datasource'] = {'source': 'invoices'} +users_invoices["url"] = 'users//invoices' +users_invoices["datasource"] = {"source": "invoices"} users_required_invoices = copy.deepcopy(required_invoices) -users_required_invoices['url'] =\ - 'users//required_invoices' -users_required_invoices['datasource'] = {'source': 'required_invoices'} +users_required_invoices[ + "url" +] = 'users//required_invoices' +users_required_invoices["datasource"] = {"source": "required_invoices"} users_searches = copy.deepcopy(invoices) -users_searches['datasource'] = {'source': 'invoices'} -users_searches['url'] = \ - 'users//saved_searches' +users_searches["datasource"] = {"source": "invoices"} +users_searches["url"] = 'users//saved_searches' internal_transactions = { - 'resource_methods': ['GET'], - 'item_methods': ['GET'], - 'internal_resource': True + "resource_methods": ["GET"], + "item_methods": ["GET"], + "internal_resource": True, } ids = { - 'query_objectid_as_string': True, - 'item_lookup_field': 'id', - 'resource_methods': ['POST', 'GET'], - 'schema': { - 'id': {'type': 'string'}, - 'name': {'type': 'string'} - } + "query_objectid_as_string": True, + "item_lookup_field": "id", + "resource_methods": ["POST", "GET"], + "schema": {"id": {"type": "string"}, "name": {"type": "string"}}, } login = { - 'item_title': 'login', - 'url': 'login', - 'datasource': { - 'projection': { - 'password': 0 - } + "item_title": "login", + "url": "login", + "datasource": {"projection": {"password": 0}}, + "schema": { + "email": {"type": "string", "required": True, "unique": True}, + "password": {"type": "string", "required": True}, }, - 'schema': { - 'email': { - 'type': 'string', - 'required': True, - 'unique': True - }, - 'password': { - 'type': 'string', - 'required': True - } - } } # This resource is used to test resource-specific id fields. products = { - 'id_field': 'sku', - 'item_lookup_field': 'sku', - 'item_url': 'regex("[A-Z]+")', - 'schema': { - 'sku': { - 'type': 'string', - 'maxlength': 16 - }, - 'title': { - 'type': 'string', - 'minlength': 4, - 'maxlength': 32 - }, - 'parent_product': { - 'type': 'string', - 'data_relation': {'resource': 'products'} - } - } + "id_field": "sku", + "item_lookup_field": "sku", + "item_url": 'regex("[A-Z]+")', + "schema": { + "sku": {"type": "string", "maxlength": 16}, + "title": {"type": "string", "minlength": 4, "maxlength": 32}, + "parent_product": {"type": "string", "data_relation": {"resource": "products"}}, + }, } child_products = copy.deepcopy(products) -child_products['url'] = 'products//children' -child_products['datasource'] = {'source': 'products'} +child_products["url"] = 'products//children' +child_products["datasource"] = {"source": "products"} exclusion = copy.deepcopy(contacts) -exclusion['url'] = 'exclusion' -exclusion['soft_delete'] = True -exclusion['datasource']['source'] = 'contacts' -exclusion['datasource']['projection'] = {'int': 0} +exclusion["url"] = "exclusion" +exclusion["soft_delete"] = True +exclusion["datasource"]["source"] = "contacts" +exclusion["datasource"]["projection"] = {"int": 0} DOMAIN = { - 'disabled_bulk': disabled_bulk, - 'contacts': contacts, - 'users': users, - 'users_overseas': users_overseas, - 'contacts_hide_born': contacts_hide_born, - 'contacts_hide_media': contacts_hide_media, - 'invoices': invoices, - 'versioned_invoices': versioned_invoices, - 'required_invoices': required_invoices, - 'payments': payments, - 'empty': empty, - 'restricted': user_restricted_access, - 'peopleinvoices': users_invoices, - 'peoplerequiredinvoices': users_required_invoices, - 'peoplesearches': users_searches, - 'companies': companies, - 'internal_transactions': internal_transactions, - 'ids': ids, - 'login': login, - 'products': products, - 'child_products': child_products, - 'exclusion': exclusion, + "disabled_bulk": disabled_bulk, + "contacts": contacts, + "users": users, + "users_overseas": users_overseas, + "contacts_hide_born": contacts_hide_born, + "contacts_hide_media": contacts_hide_media, + "invoices": invoices, + "versioned_invoices": versioned_invoices, + "required_invoices": required_invoices, + "payments": payments, + "empty": empty, + "restricted": user_restricted_access, + "peopleinvoices": users_invoices, + "peoplerequiredinvoices": users_required_invoices, + "peoplesearches": users_searches, + "companies": companies, + "internal_transactions": internal_transactions, + "ids": ids, + "login": login, + "products": products, + "child_products": child_products, + "exclusion": exclusion, } diff --git a/eve/tests/test_settings_env.py b/eve/tests/test_settings_env.py index 16554e543..3a3b1e107 100644 --- a/eve/tests/test_settings_env.py +++ b/eve/tests/test_settings_env.py @@ -4,4 +4,4 @@ # to try to load with environmental variable in # test_existing_env_config() test case -DOMAIN = {'env_domain': {}} +DOMAIN = {"env_domain": {}} diff --git a/eve/tests/test_version.py b/eve/tests/test_version.py index 19e6e336e..9397b398a 100644 --- a/eve/tests/test_version.py +++ b/eve/tests/test_version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -API_VERSION = 'v1' -DOMAIN = {'contacts': {}} +API_VERSION = "v1" +DOMAIN = {"contacts": {}} diff --git a/eve/tests/utils.py b/eve/tests/utils.py index 72401ca89..5d98b256c 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -5,9 +5,19 @@ from bson.json_util import dumps from datetime import datetime, timedelta from eve.tests import TestBase -from eve.utils import parse_request, str_to_date, config, weak_date, \ - date_to_str, querydef, document_etag, extract_key_values, \ - debug_error_message, validate_filters, import_from_string +from eve.utils import ( + parse_request, + str_to_date, + config, + weak_date, + date_to_str, + querydef, + document_etag, + extract_key_values, + debug_error_message, + validate_filters, + import_from_string, +) class TestUtils(TestBase): @@ -19,265 +29,260 @@ class TestUtils(TestBase): def setUp(self): super(TestUtils, self).setUp() self.dt_fmt = config.DATE_FORMAT - self.datestr = 'Tue, 18 Sep 2012 10:12:30 GMT' + self.datestr = "Tue, 18 Sep 2012 10:12:30 GMT" self.valid = datetime.strptime(self.datestr, self.dt_fmt) - self.etag = '56eaadbbd9fa287e7270cf13a41083c94f52ab9b' + self.etag = "56eaadbbd9fa287e7270cf13a41083c94f52ab9b" def test_parse_request_where(self): - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['ref'] + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = ["ref"] with self.app.test_request_context(): self.assertEqual(parse_request(self.known_resource).where, None) - with self.app.test_request_context('/?where=hello'): - self.assertEqual(parse_request(self.known_resource).where, 'hello') + with self.app.test_request_context("/?where=hello"): + self.assertEqual(parse_request(self.known_resource).where, "hello") def test_parse_request_sort(self): with self.app.test_request_context(): self.assertEqual(parse_request(self.known_resource).sort, None) - with self.app.test_request_context('/?sort=hello'): - self.assertEqual(parse_request(self.known_resource).sort, 'hello') + with self.app.test_request_context("/?sort=hello"): + self.assertEqual(parse_request(self.known_resource).sort, "hello") def test_parse_request_page(self): with self.app.test_request_context(): self.assertEqual(parse_request(self.known_resource).page, 1) - with self.app.test_request_context('/?page=2'): + with self.app.test_request_context("/?page=2"): self.assertEqual(parse_request(self.known_resource).page, 2) - with self.app.test_request_context('/?page=-1'): + with self.app.test_request_context("/?page=-1"): self.assertEqual(parse_request(self.known_resource).page, 1) - with self.app.test_request_context('/?page=0'): + with self.app.test_request_context("/?page=0"): self.assertEqual(parse_request(self.known_resource).page, 1) - with self.app.test_request_context('/?page=1.1'): + with self.app.test_request_context("/?page=1.1"): self.assertEqual(parse_request(self.known_resource).page, 1) - with self.app.test_request_context('/?page=string'): + with self.app.test_request_context("/?page=string"): self.assertEqual(parse_request(self.known_resource).page, 1) def test_parse_request_max_results(self): default = config.PAGINATION_DEFAULT limit = config.PAGINATION_LIMIT with self.app.test_request_context(): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=%d' % (limit + 1)): - self.assertEqual(parse_request(self.known_resource).max_results, - limit) - with self.app.test_request_context('/?max_results=2'): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=%d" % (limit + 1)): + self.assertEqual(parse_request(self.known_resource).max_results, limit) + with self.app.test_request_context("/?max_results=2"): self.assertEqual(parse_request(self.known_resource).max_results, 2) - with self.app.test_request_context('/?max_results=-1'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=0'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=1.1'): + with self.app.test_request_context("/?max_results=-1"): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=0"): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=1.1"): self.assertEqual(parse_request(self.known_resource).max_results, 1) - with self.app.test_request_context('/?max_results=string'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) + with self.app.test_request_context("/?max_results=string"): + self.assertEqual(parse_request(self.known_resource).max_results, default) def test_parse_request_max_results_disabled_pagination(self): - self.app.config['DOMAIN'][self.known_resource]['pagination'] = False + self.app.config["DOMAIN"][self.known_resource]["pagination"] = False default = 0 limit = config.PAGINATION_LIMIT with self.app.test_request_context(): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=%d' % (limit + 1)): - self.assertEqual(parse_request(self.known_resource).max_results, - limit + 1) - with self.app.test_request_context('/?max_results=2'): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=%d" % (limit + 1)): + self.assertEqual(parse_request(self.known_resource).max_results, limit + 1) + with self.app.test_request_context("/?max_results=2"): self.assertEqual(parse_request(self.known_resource).max_results, 2) - with self.app.test_request_context('/?max_results=-1'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=0'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) - with self.app.test_request_context('/?max_results=1.1'): + with self.app.test_request_context("/?max_results=-1"): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=0"): + self.assertEqual(parse_request(self.known_resource).max_results, default) + with self.app.test_request_context("/?max_results=1.1"): self.assertEqual(parse_request(self.known_resource).max_results, 1) - with self.app.test_request_context('/?max_results=string'): - self.assertEqual(parse_request(self.known_resource).max_results, - default) + with self.app.test_request_context("/?max_results=string"): + self.assertEqual(parse_request(self.known_resource).max_results, default) def test_parse_request_if_modified_since(self): - ims = 'If-Modified-Since' + ims = "If-Modified-Since" with self.app.test_request_context(): - self.assertEqual(parse_request( - self.known_resource).if_modified_since, None) + self.assertEqual(parse_request(self.known_resource).if_modified_since, None) with self.app.test_request_context(headers=None): - self.assertEqual( - parse_request(self.known_resource).if_modified_since, None) + self.assertEqual(parse_request(self.known_resource).if_modified_since, None) with self.app.test_request_context(headers={ims: self.datestr}): self.assertEqual( parse_request(self.known_resource).if_modified_since, - self.valid + timedelta(seconds=1)) - with self.app.test_request_context(headers={ims: 'not-a-date'}): + self.valid + timedelta(seconds=1), + ) + with self.app.test_request_context(headers={ims: "not-a-date"}): self.assertRaises(ValueError, parse_request, self.known_resource) with self.app.test_request_context( - headers={ims: - self.datestr.replace('GMT', 'UTC')}): + headers={ims: self.datestr.replace("GMT", "UTC")} + ): self.assertRaises(ValueError, parse_request, self.known_resource) self.assertRaises(ValueError, parse_request, self.known_resource) def test_parse_request_if_none_match(self): with self.app.test_request_context(): - self.assertEqual(parse_request(self.known_resource).if_none_match, - None) + self.assertEqual(parse_request(self.known_resource).if_none_match, None) with self.app.test_request_context(headers=None): - self.assertEqual(parse_request(self.known_resource).if_none_match, - None) - with self.app.test_request_context(headers={'If-None-Match': - self.etag}): - self.assertEqual(parse_request(self.known_resource).if_none_match, - self.etag) + self.assertEqual(parse_request(self.known_resource).if_none_match, None) + with self.app.test_request_context(headers={"If-None-Match": self.etag}): + self.assertEqual( + parse_request(self.known_resource).if_none_match, self.etag + ) def test_parse_request_if_match(self): with self.app.test_request_context(): self.assertEqual(parse_request(self.known_resource).if_match, None) with self.app.test_request_context(headers=None): self.assertEqual(parse_request(self.known_resource).if_match, None) - with self.app.test_request_context(headers={'If-Match': self.etag}): - self.assertEqual(parse_request(self.known_resource).if_match, - self.etag) + with self.app.test_request_context(headers={"If-Match": self.etag}): + self.assertEqual(parse_request(self.known_resource).if_match, self.etag) def test_weak_date(self): with self.app.test_request_context(): - self.app.config['DATE_FORMAT'] = '%Y-%m-%d' - self.assertEqual(weak_date(self.datestr), self.valid + - timedelta(seconds=1)) + self.app.config["DATE_FORMAT"] = "%Y-%m-%d" + self.assertEqual(weak_date(self.datestr), self.valid + timedelta(seconds=1)) def test_str_to_date(self): self.assertEqual(str_to_date(self.datestr), self.valid) - self.assertRaises(ValueError, str_to_date, 'not-a-date') - self.assertRaises(ValueError, str_to_date, - self.datestr.replace('GMT', 'UTC')) + self.assertRaises(ValueError, str_to_date, "not-a-date") + self.assertRaises(ValueError, str_to_date, self.datestr.replace("GMT", "UTC")) def test_date_to_str(self): self.assertEqual(date_to_str(self.valid), self.datestr) def test_querydef(self): - self.assertEqual(querydef(max_results=10), '?max_results=10') - self.assertEqual(querydef(page=10), '?page=10') - self.assertEqual(querydef(where='wherepart'), '?where=wherepart') - self.assertEqual(querydef(sort='sortpart'), '?sort=sortpart') - - self.assertEqual(querydef(where='wherepart', sort='sortpart'), - '?where=wherepart&sort=sortpart') - self.assertEqual(querydef(max_results=10, sort='sortpart'), - '?max_results=10&sort=sortpart') + self.assertEqual(querydef(max_results=10), "?max_results=10") + self.assertEqual(querydef(page=10), "?page=10") + self.assertEqual(querydef(where="wherepart"), "?where=wherepart") + self.assertEqual(querydef(sort="sortpart"), "?sort=sortpart") + + self.assertEqual( + querydef(where="wherepart", sort="sortpart"), + "?where=wherepart&sort=sortpart", + ) + self.assertEqual( + querydef(max_results=10, sort="sortpart"), "?max_results=10&sort=sortpart" + ) def test_document_etag(self): - test = {'key1': 'value1', 'another': 'value2'} - challenge = dumps(test, sort_keys=True).encode('utf-8') + test = {"key1": "value1", "another": "value2"} + challenge = dumps(test, sort_keys=True).encode("utf-8") with self.app.test_request_context(): - self.assertEqual(hashlib.sha1(challenge).hexdigest(), - document_etag(test)) + self.assertEqual(hashlib.sha1(challenge).hexdigest(), document_etag(test)) def test_document_etag_ignore_fields(self): - test = {'key1': 'value1', 'key2': 'value2'} + test = {"key1": "value1", "key2": "value2"} ignore_fields = ["key2"] - test_without_ignore = {'key1': 'value1'} - challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8') + test_without_ignore = {"key1": "value1"} + challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") with self.app.test_request_context(): - self.assertEqual(hashlib.sha1(challenge).hexdigest(), - document_etag(test, ignore_fields)) + self.assertEqual( + hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) + ) # not required fields can not be present - test = {'key1': 'value1', 'key2': 'value2'} + test = {"key1": "value1", "key2": "value2"} ignore_fields = ["key3"] - test_without_ignore = {'key1': 'value1', 'key2': 'value2'} - challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8') + test_without_ignore = {"key1": "value1", "key2": "value2"} + challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") with self.app.test_request_context(): - self.assertEqual(hashlib.sha1(challenge).hexdigest(), - document_etag(test, ignore_fields)) + self.assertEqual( + hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) + ) # ignore fiels nested using doting notation - test = {'key1': 'value1', 'dict': {'key2': 'value2', 'key3': 'value3'}} - ignore_fields = ['dict.key2'] - test_without_ignore = {'key1': 'value1', 'dict': {'key3': 'value3'}} - challenge = dumps(test_without_ignore, sort_keys=True).encode('utf-8') + test = {"key1": "value1", "dict": {"key2": "value2", "key3": "value3"}} + ignore_fields = ["dict.key2"] + test_without_ignore = {"key1": "value1", "dict": {"key3": "value3"}} + challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") with self.app.test_request_context(): - self.assertEqual(hashlib.sha1(challenge).hexdigest(), - document_etag(test, ignore_fields)) + self.assertEqual( + hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) + ) def test_extract_key_values(self): test = { - 'key1': 'value1', - 'key2': { - 'key1': 'value2', - 'nested': { - 'key1': 'value3' - } - } + "key1": "value1", + "key2": {"key1": "value2", "nested": {"key1": "value3"}}, } - self.assertEqual(list(extract_key_values('key1', test)), - ['value1', 'value2', 'value3']) + self.assertEqual( + list(extract_key_values("key1", test)), ["value1", "value2", "value3"] + ) def test_debug_error_message(self): with self.app.test_request_context(): - self.app.config['DEBUG'] = False - self.assertEqual(debug_error_message('An error message'), None) - self.app.config['DEBUG'] = True - self.assertEqual(debug_error_message('An error message'), - 'An error message') + self.app.config["DEBUG"] = False + self.assertEqual(debug_error_message("An error message"), None) + self.app.config["DEBUG"] = True + self.assertEqual( + debug_error_message("An error message"), "An error message" + ) def test_validate_filters_when_custom_types_are_used(self): # Filters validation should operate on the active validator instance, # not on Cerberus' standard one. See #1154. - self.app.config['VALIDATE_FILTERS'] = True - response, status = self.get(self.known_resource, - query='?where={"tid":"1234"}') + self.app.config["VALIDATE_FILTERS"] = True + response, status = self.get(self.known_resource, query='?where={"tid":"1234"}') self.assert400(status) - self.assertTrue("filter on 'tid' is invalid" in - response['_error']['message']) + self.assertTrue("filter on 'tid' is invalid" in response["_error"]["message"]) response, status = self.get( - self.known_resource, - query='?where={"tid":"5a1154523a6bcc1d245e143d"}') + self.known_resource, query='?where={"tid":"5a1154523a6bcc1d245e143d"}' + ) self.assert200(status) def test_validate_filters(self): - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = [] + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = [] with self.app.test_request_context(): - self.assertTrue('key' in validate_filters( - {'key': 'val'}, - self.known_resource)) - self.assertTrue('key' in validate_filters( - {'key': ['val1', 'val2']}, - self.known_resource)) - self.assertTrue('key' in validate_filters( - {'key': {'$in': ['val1', 'val2']}}, - self.known_resource)) - self.assertTrue('key' in validate_filters( - {'$or': [{'key': 'val1'}, {'key': 'val2'}]}, - self.known_resource)) - self.assertTrue('$or' in validate_filters( - {'$or': 'val'}, - self.known_resource)) - self.assertTrue('$or' in validate_filters( - {'$or': {'key': 'val1'}}, - self.known_resource)) - self.assertTrue('$or' in validate_filters( - {'$or': ['val']}, - self.known_resource)) - - self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \ - ['key'] + self.assertTrue( + "key" in validate_filters({"key": "val"}, self.known_resource) + ) + self.assertTrue( + "key" + in validate_filters({"key": ["val1", "val2"]}, self.known_resource) + ) + self.assertTrue( + "key" + in validate_filters( + {"key": {"$in": ["val1", "val2"]}}, self.known_resource + ) + ) + self.assertTrue( + "key" + in validate_filters( + {"$or": [{"key": "val1"}, {"key": "val2"}]}, self.known_resource + ) + ) + self.assertTrue( + "$or" in validate_filters({"$or": "val"}, self.known_resource) + ) + self.assertTrue( + "$or" in validate_filters({"$or": {"key": "val1"}}, self.known_resource) + ) + self.assertTrue( + "$or" in validate_filters({"$or": ["val"]}, self.known_resource) + ) + + self.app.config["DOMAIN"][self.known_resource]["allowed_filters"] = ["key"] with self.app.test_request_context(): - self.assertTrue(validate_filters( - {'key': 'val'}, - self.known_resource) is None) - self.assertTrue(validate_filters( - {'key': ['val1', 'val2']}, - self.known_resource) is None) - self.assertTrue(validate_filters( - {'key': {'$in': ['val1', 'val2']}}, - self.known_resource) is None) - self.assertTrue(validate_filters( - {'$or': [{'key': 'val1'}, {'key': 'val2'}]}, - self.known_resource) is None) + self.assertTrue( + validate_filters({"key": "val"}, self.known_resource) is None + ) + self.assertTrue( + validate_filters({"key": ["val1", "val2"]}, self.known_resource) is None + ) + self.assertTrue( + validate_filters( + {"key": {"$in": ["val1", "val2"]}}, self.known_resource + ) + is None + ) + self.assertTrue( + validate_filters( + {"$or": [{"key": "val1"}, {"key": "val2"}]}, self.known_resource + ) + is None + ) def test_import_from_string(self): - dt = import_from_string('datetime.datetime') + dt = import_from_string("datetime.datetime") self.assertEqual(dt, datetime) @@ -294,6 +299,7 @@ class DummyEvent(object): assert app.on_my_event.called[0] == expected_param_0 """ + def __init__(self, check, deepcopy=False): """ :param check: method checking the state of something during the event. diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 377662ab0..a8c8b4802 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -11,19 +11,17 @@ class TestVersioningBase(TestBase): def setUp(self): - self.versioned_field = 'ref' - self.unversioned_field = 'prog' + self.versioned_field = "ref" + self.unversioned_field = "prog" self.fields = [self.versioned_field, self.unversioned_field] super(TestVersioningBase, self).setUp() - self.id_field = self.domain[self.known_resource]['id_field'] - self.version_field = self.app.config['VERSION'] - self.latest_version_field = self.app.config['LATEST_VERSION'] - self.document_id_field = (self.id_field + - self.app.config['VERSION_ID_SUFFIX']) - self.known_resource_shadow = self.known_resource + \ - self.app.config['VERSIONS'] + self.id_field = self.domain[self.known_resource]["id_field"] + self.version_field = self.app.config["VERSION"] + self.latest_version_field = self.app.config["LATEST_VERSION"] + self.document_id_field = self.id_field + self.app.config["VERSION_ID_SUFFIX"] + self.known_resource_shadow = self.known_resource + self.app.config["VERSIONS"] self._db = self.connection[MONGO_DBNAME] @@ -32,52 +30,51 @@ def tearDown(self): self.connection.close() def enableVersioning(self, partial=False): - del(self.domain['contacts']['schema']['title']['default']) - del(self.domain['contacts']['schema']['dependency_field1']['default']) - del(self.domain['contacts']['schema']['read_only_field']['default']) - del(self.domain['contacts']['schema']['dict_with_read_only'] - ['schema']['read_only_in_dict']['default']) + del (self.domain["contacts"]["schema"]["title"]["default"]) + del (self.domain["contacts"]["schema"]["dependency_field1"]["default"]) + del (self.domain["contacts"]["schema"]["read_only_field"]["default"]) + del ( + self.domain["contacts"]["schema"]["dict_with_read_only"]["schema"][ + "read_only_in_dict" + ]["default"] + ) if partial is True: - contact_schema = self.domain['contacts']['schema'] - contact_schema[self.unversioned_field]['versioned'] = False + contact_schema = self.domain["contacts"]["schema"] + contact_schema[self.unversioned_field]["versioned"] = False domain = copy.copy(self.domain) for resource, settings in domain.items(): - settings['versioning'] = True - settings['datasource'].pop('projection', None) + settings["versioning"] = True + settings["datasource"].pop("projection", None) self.app.register_resource(resource, settings) - def enableDataVersionRelation(self, embeddable=True, custom_field=None, - custom_field_type='string'): + def enableDataVersionRelation( + self, embeddable=True, custom_field=None, custom_field_type="string" + ): field = { - 'type': 'dict', - 'schema': { - self.app.config['VERSION']: {'type': 'integer'} - }, - 'data_relation': { - 'version': True, - 'resource': 'contacts' - } + "type": "dict", + "schema": {self.app.config["VERSION"]: {"type": "integer"}}, + "data_relation": {"version": True, "resource": "contacts"}, } if custom_field is None: - field['schema'][self.id_field] = {'type': 'objectid'} + field["schema"][self.id_field] = {"type": "objectid"} else: - field['schema'][custom_field] = {'type': custom_field_type} - field['data_relation']['field'] = custom_field + field["schema"][custom_field] = {"type": custom_field_type} + field["data_relation"]["field"] = custom_field if embeddable is True: - field['data_relation']['embeddable'] = True + field["data_relation"]["embeddable"] = True - self.domain['invoices']['schema']['person'] = field + self.domain["invoices"]["schema"]["person"] = field def enableSoftDelete(self): - self.app.config['SOFT_DELETE'] = True + self.app.config["SOFT_DELETE"] = True domain = copy.copy(self.domain) for resource, settings in domain.items(): # rebuild resource settings for soft delete - del settings['soft_delete'] + del settings["soft_delete"] self.app.register_resource(resource, settings) - self.deleted_field = self.app.config['DELETED'] + self.deleted_field = self.app.config["DELETED"] def assertEqualFields(self, obj1, obj2, fields): for field in fields: @@ -91,8 +88,7 @@ def assertLatestVersion(self, response, latest_version): self.assertTrue(self.latest_version_field in response) self.assertEqual(response[self.latest_version_field], latest_version) - def assertDocumentVersionFields( - self, response, version, latest_version=None): + def assertDocumentVersionFields(self, response, version, latest_version=None): self.assertVersion(response, version) if latest_version is None: latest_version = version @@ -103,8 +99,7 @@ def directGetDocument(self, _id): def directGetShadowDocument(self, _id, version): return self._db[self.known_resource_shadow].find_one( - {self.document_id_field: ObjectId(_id), - self.app.config['VERSION']: version} + {self.document_id_field: ObjectId(_id), self.app.config["VERSION"]: version} ) def countDocuments(self, _id=None): @@ -135,12 +130,12 @@ def setUp(self): # create some dummy contacts to use for versioning tests self.item = { - self.versioned_field: 'ref value 1..............', - self.unversioned_field: 123 + self.versioned_field: "ref value 1..............", + self.unversioned_field: 123, } self.item_change = { - self.versioned_field: 'ref value 2..............', - self.unversioned_field: 456 + self.versioned_field: "ref value 2..............", + self.unversioned_field: 456, } def insertTestData(self): @@ -148,9 +143,10 @@ def insertTestData(self): self.assert201(status) self.item_id = contact[self.id_field] self.item_etag = contact[ETAG] - self.item_id_url = ('/%s/%s' % - (self.domain[self.known_resource]['url'], - self.item_id)) + self.item_id_url = "/%s/%s" % ( + self.domain[self.known_resource]["url"], + self.item_id, + ) def assertPrimaryAndShadowDocuments(self, _id, version, partial=False): # verify primary document fields @@ -165,25 +161,26 @@ def assertPrimaryAndShadowDocuments(self, _id, version, partial=False): self.assertTrue(shadow_document is not None) self.assertTrue(self.versioned_field in shadow_document) self.assertEqual( - document[self.versioned_field], - shadow_document[self.versioned_field]) + document[self.versioned_field], shadow_document[self.versioned_field] + ) if partial is True: self.assertFalse(self.unversioned_field in shadow_document) else: self.assertTrue(self.unversioned_field in shadow_document) self.assertEqual( document[self.unversioned_field], - shadow_document[self.unversioned_field]) + shadow_document[self.unversioned_field], + ) # verify meta fields self.assertTrue(shadow_document[self.version_field] == version) self.assertTrue(self.document_id_field in shadow_document) self.assertEqual( - document[self.id_field], - shadow_document[self.document_id_field]) + document[self.id_field], shadow_document[self.document_id_field] + ) self.assertTrue(self.id_field in shadow_document) - self.assertTrue(self.app.config['LAST_UPDATED'] in shadow_document) - self.assertTrue(self.app.config['ETAG'] in shadow_document) + self.assertTrue(self.app.config["LAST_UPDATED"] in shadow_document) + self.assertTrue(self.app.config["ETAG"] in shadow_document) # verify that no unexpected fields exist num_meta_fields = 5 # see previous block @@ -196,23 +193,21 @@ def assertHateoasLinks(self, links, version_param): """ Makes sure links for `self`, `collection`, and `parent` point to the right place. """ - self_url = links['self']['href'] - coll_url = links['collection']['href'] - prnt_url = links['parent']['href'] - self.assertTrue('?version=%s' % (str(version_param)) in self_url) - if version_param in ('all', 'diffs'): - self.assertEqual(self_url.split('?')[0], coll_url) - self.assertEqual(coll_url.rsplit('/', 1)[0], prnt_url) + self_url = links["self"]["href"] + coll_url = links["collection"]["href"] + prnt_url = links["parent"]["href"] + self.assertTrue("?version=%s" % (str(version_param)) in self_url) + if version_param in ("all", "diffs"): + self.assertEqual(self_url.split("?")[0], coll_url) + self.assertEqual(coll_url.rsplit("/", 1)[0], prnt_url) else: - self.assertEqual('%s?version=all' % self_url.split('?')[0], - coll_url) - self.assertEqual(coll_url.split('?')[0], prnt_url) + self.assertEqual("%s?version=all" % self_url.split("?")[0], coll_url) + self.assertEqual(coll_url.split("?")[0], prnt_url) def do_test_get(self): - query = '?where={"%s":"%s"}' % \ - (self.id_field, self.item_id) + query = '?where={"%s":"%s"}' % (self.id_field, self.item_id) response, status = self.get(self.known_resource, query=query) - response = response[self.app.config['ITEMS']][0] + response = response[self.app.config["ITEMS"]][0] # get always returns the latest version of a document self.assert200(status) @@ -221,34 +216,38 @@ def do_test_get(self): def do_test_getitem(self, partial): # put a second version - response, status = self.put(self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) if partial is True: # build expected response since the state of version 1 will change version_1 = copy.copy(self.item) - version_1[self.unversioned_field] = \ - self.item_change[self.unversioned_field] + version_1[self.unversioned_field] = self.item_change[self.unversioned_field] else: version_1 = self.item # check the get of the first version - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=1') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=1" + ) self.assert200(status) self.assertDocumentVersionFields(response, 1, 2) self.assertEqualFields(version_1, response, self.fields) - links = response['_links'] + links = response["_links"] self.assertHateoasLinks(links, 1) # check the get of the second version - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=2') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=2" + ) self.assert200(status) self.assertDocumentVersionFields(response, 2) self.assertEqualFields(self.item_change, response, self.fields) - links = response['_links'] + links = response["_links"] self.assertHateoasLinks(links, 2) # check the get without version specified and make sure it is version 2 @@ -258,8 +257,7 @@ def do_test_getitem(self, partial): self.assertEqualFields(self.item_change, response, self.fields) def do_test_post(self, partial): - response, status = self.post( - self.known_resource_url, data=self.item_change) + response, status = self.post(self.known_resource_url, data=self.item_change) self.assert201(status) _id = response[self.id_field] self.assertPrimaryAndShadowDocuments(_id, 1, partial=partial) @@ -273,8 +271,11 @@ def do_test_multi_post(self): self.assertTrue(True) def do_test_put(self, partial): - response, status = self.put(self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) self.assertPrimaryAndShadowDocuments(self.item_id, 2, partial=partial) @@ -285,8 +286,10 @@ def do_test_put(self, partial): def do_test_patch(self, partial): response, status = self.patch( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) self.assertPrimaryAndShadowDocuments(self.item_id, 2, partial=partial) @@ -349,7 +352,8 @@ def test_getitem_version_unknown(self): version. """ response, status = self.get( - self.known_resource, item=self.item_id, query='?version=2') + self.known_resource, item=self.item_id, query="?version=2" + ) self.assert404(status) def test_getitem_version_bad_format(self): @@ -357,7 +361,8 @@ def test_getitem_version_bad_format(self): version. """ response, status = self.get( - self.known_resource, item=self.item_id, query='?version=bad') + self.known_resource, item=self.item_id, query="?version=bad" + ) self.assert400(status) def test_getitem_version_all(self): @@ -366,22 +371,29 @@ def test_getitem_version_all(self): """ meta_fields = self.fields + [ self.id_field, - self.app.config['LAST_UPDATED'], self.app.config['ETAG'], - self.app.config['DATE_CREATED'], self.app.config['LINKS'], - self.version_field, self.latest_version_field] + self.app.config["LAST_UPDATED"], + self.app.config["ETAG"], + self.app.config["DATE_CREATED"], + self.app.config["LINKS"], + self.version_field, + self.latest_version_field, + ] # put a second version response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) - etag2 = response[self.app.config['ETAG']] + etag2 = response[self.app.config["ETAG"]] # get query response, status = self.get( - self.known_resource, item=self.item_id, query='?version=all') + self.known_resource, item=self.item_id, query="?version=all" + ) self.assert200(status) - items = response[self.app.config['ITEMS']] + items = response[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) # check the get of the first version @@ -389,22 +401,24 @@ def test_getitem_version_all(self): self.assertEqualFields(self.item, items[0], self.fields) self.assertTrue(field in items[0] for field in meta_fields) self.assertEqual(len(items[0].keys()), len(meta_fields)) - self.assertEqual(items[0][self.app.config['ETAG']], self.item_etag) + self.assertEqual(items[0][self.app.config["ETAG"]], self.item_etag) # # check the get of the second version self.assertDocumentVersionFields(items[1], 2) self.assertEqualFields(self.item_change, items[1], self.fields) self.assertTrue(field in items[1] for field in meta_fields) self.assertEqual(len(items[1].keys()), len(meta_fields)) - self.assertEqual(items[1][self.app.config['ETAG']], etag2) + self.assertEqual(items[1][self.app.config["ETAG"]], etag2) # check the `self` links for both versions - self_href = items[0]['_links']['self']['href'] - self.assertEqual(int(self_href.split('?version=')[1]), - items[0][self.version_field]) - self_href = items[1]['_links']['self']['href'] - self.assertEqual(int(self_href.split('?version=')[1]), - items[1][self.version_field]) + self_href = items[0]["_links"]["self"]["href"] + self.assertEqual( + int(self_href.split("?version=")[1]), items[0][self.version_field] + ) + self_href = items[1]["_links"]["self"]["href"] + self.assertEqual( + int(self_href.split("?version=")[1]), items[1][self.version_field] + ) def test_getitem_version_pagination(self): """ Verify that `?version=all` and `?version=diffs` display pagination @@ -412,23 +426,28 @@ def test_getitem_version_pagination(self): """ # create many versions response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) for n in range(100): response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', response[self.app.config['ETAG']])]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", response[self.app.config["ETAG"]])], + ) # get 2nd page of results page = 2 - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=all&page=%d' % page) - links = response['_links'] + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=all&page=%d" % page + ) + links = response["_links"] self.assertNextLink(links, 3) self.assertPrevLink(links, 1) self.assertLastLink(links, 5) self.assertPagination(response, 2, 102, 25) - self.assertHateoasLinks(links, 'all') + self.assertHateoasLinks(links, "all") def test_on_fetched_item(self): """ Verify that on_fetched_item events are fired for versioned @@ -436,30 +455,29 @@ def test_on_fetched_item(self): """ devent = DummyEvent(lambda: True) self.app.on_fetched_item += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=1') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=1" + ) self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual( - self.item_id, - str(devent.called[1][self.id_field])) + self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) self.assertEqual(2, len(devent.called)) # check for ?version=all requests devent = DummyEvent(lambda: True) self.app.on_fetched_item += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=all') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=all" + ) self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual( - self.item_id, - str(devent.called[1][self.id_field])) + self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) self.assertEqual(2, len(devent.called)) # check for ?version=diffs requests devent = DummyEvent(lambda: True) self.app.on_fetched_item += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=diffs') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=diffs" + ) self.assertEqual(None, devent.called) def test_on_fetched_item_contacts(self): @@ -468,28 +486,27 @@ def test_on_fetched_item_contacts(self): """ devent = DummyEvent(lambda: True) self.app.on_fetched_item_contacts += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=1') - self.assertEqual( - self.item_id, - str(devent.called[0][self.id_field])) + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=1" + ) + self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(1, len(devent.called)) # check for ?version=all requests devent = DummyEvent(lambda: True) self.app.on_fetched_item_contacts += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=all') - self.assertEqual( - self.item_id, - str(devent.called[0][self.id_field])) + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=all" + ) + self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(1, len(devent.called)) # check for ?version=diffs requests devent = DummyEvent(lambda: True) self.app.on_fetched_item_contacts += devent - response, status = self.get(self.known_resource, item=self.item_id, - query='?version=diffs') + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=diffs" + ) self.assertEqual(None, devent.called) # TODO: also test with HATEOS off @@ -500,22 +517,29 @@ def test_getitem_version_diffs(self): """ meta_fields = self.fields + [ self.id_field, - self.app.config['LAST_UPDATED'], self.app.config['ETAG'], - self.app.config['DATE_CREATED'], self.app.config['LINKS'], - self.version_field, self.latest_version_field] + self.app.config["LAST_UPDATED"], + self.app.config["ETAG"], + self.app.config["DATE_CREATED"], + self.app.config["LINKS"], + self.version_field, + self.latest_version_field, + ] # put a second version response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) - etag2 = response[self.app.config['ETAG']] + etag2 = response[self.app.config["ETAG"]] # get query response, status = self.get( - self.known_resource, item=self.item_id, query='?version=diffs') + self.known_resource, item=self.item_id, query="?version=diffs" + ) self.assert200(status) - items = response[self.app.config['ITEMS']] + items = response[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) # check the get of the first version @@ -523,23 +547,25 @@ def test_getitem_version_diffs(self): self.assertEqualFields(self.item, items[0], self.fields) self.assertTrue(field in items[0] for field in meta_fields) self.assertEqual(len(items[0].keys()), len(meta_fields)) - self.assertEqual(items[0][self.app.config['ETAG']], self.item_etag) + self.assertEqual(items[0][self.app.config["ETAG"]], self.item_etag) # # check the get of the second version self.assertVersion(items[1], 2) self.assertEqualFields(self.item_change, items[1], self.fields) changed_fields = self.fields + [ self.version_field, - self.app.config['ETAG'], - self.app.config['LINKS']] + self.app.config["ETAG"], + self.app.config["LINKS"], + ] for field in changed_fields: self.assertTrue(field in items[1], "%s not in diffs" % field) # since the test routine happens so fast, `LAST_UPDATED` may or may not # be in the diff (the date output only has a one second resolution) self.assertTrue( - len(items[1].keys()) == len(changed_fields) or - len(items[1].keys()) == len(changed_fields) + 1) - self.assertEqual(items[1][self.app.config['ETAG']], etag2) + len(items[1].keys()) == len(changed_fields) + or len(items[1].keys()) == len(changed_fields) + 1 + ) + self.assertEqual(items[1][self.app.config["ETAG"]], etag2) # TODO: could also verify that a 3rd iteration is a diff of the 2nd # iteration and not a diff of the 1st iteration by mistake... @@ -551,8 +577,10 @@ def test_getitem_projection(self): """ # test inclusive projection response, status = self.get( - self.known_resource, item=self.item_id, - query='?projection={"%s": 1}' % self.unversioned_field) + self.known_resource, + item=self.item_id, + query='?projection={"%s": 1}' % self.unversioned_field, + ) self.assert200(status) self.assertTrue(self.unversioned_field in response) self.assertFalse(self.versioned_field in response) @@ -561,8 +589,10 @@ def test_getitem_projection(self): # test exclusive projection response, status = self.get( - self.known_resource, item=self.item_id, - query='?projection={"%s": 0}' % self.unversioned_field) + self.known_resource, + item=self.item_id, + query='?projection={"%s": 0}' % self.unversioned_field, + ) self.assert200(status) self.assertFalse(self.unversioned_field in response) self.assertTrue(self.versioned_field in response) @@ -574,17 +604,21 @@ def test_getitem_version_all_projection(self): """ # put a second version response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # test inclusive projection projection = '{"%s": 1}' % self.unversioned_field response, status = self.get( - self.known_resource, item=self.item_id, - query='?version=all&projection=%s' % projection) + self.known_resource, + item=self.item_id, + query="?version=all&projection=%s" % projection, + ) self.assert200(status) - items = response[self.app.config['ITEMS']] + items = response[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) for item in items: self.assertTrue(self.unversioned_field in item) @@ -593,20 +627,23 @@ def test_getitem_version_all_projection(self): self.assertTrue(self.latest_version_field in item) if item[self.version_field] == 1: self.assertEqual( - item[self.unversioned_field], - self.item[self.unversioned_field]) + item[self.unversioned_field], self.item[self.unversioned_field] + ) else: self.assertEqual( item[self.unversioned_field], - self.item_change[self.unversioned_field]) + self.item_change[self.unversioned_field], + ) # test exclusive projection projection = '{"%s": 0}' % self.unversioned_field response, status = self.get( - self.known_resource, item=self.item_id, - query='?version=all&projection=%s' % projection) + self.known_resource, + item=self.item_id, + query="?version=all&projection=%s" % projection, + ) self.assert200(status) - items = response[self.app.config['ITEMS']] + items = response[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) for item in items: self.assertFalse(self.unversioned_field in item) @@ -614,8 +651,7 @@ def test_getitem_version_all_projection(self): self.assertTrue(self.version_field in item) self.assertTrue(self.latest_version_field in item) - def test_getitem_version_new_latest_version_invalidates_if_modified_since( - self): + def test_getitem_version_new_latest_version_invalidates_if_modified_since(self): """Verify that a cached document version is invalidated via an 'If-Modified-Since' header when the _latest_version field has changed due to creation of a new version @@ -625,26 +661,29 @@ def test_getitem_version_new_latest_version_invalidates_if_modified_since( document, status = self.parse_response(r) self.assert200(status) self.assertEqual(document[self.latest_version_field], 1) - last_modified = r.headers.get('Last-Modified') + last_modified = r.headers.get("Last-Modified") # put a second version (after enough time has passed to expect a new # Last-Modified header) time.sleep(2) response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # get first version again and confirm Last-Modified and latest version # have been updated - r = self.test_client.get(self.item_id_url + "?version=1", headers=[ - ('If-Modified-Since', last_modified)]) + r = self.test_client.get( + self.item_id_url + "?version=1", + headers=[("If-Modified-Since", last_modified)], + ) document, status = self.parse_response(r) self.assert200(status) self.assertEqual(document[self.latest_version_field], 2) - def test_getitem_version_new_latest_version_invalidates_if_none_match( - self): + def test_getitem_version_new_latest_version_invalidates_if_none_match(self): """Verify that a cached document version is invalidated via an 'If-None-Match' header when the _latest_version field has changed due to creation of a new version @@ -654,18 +693,20 @@ def test_getitem_version_new_latest_version_invalidates_if_none_match( document, status = self.parse_response(r) self.assert200(status) self.assertEqual(document[self.latest_version_field], 1) - version1_etag = r.headers.get('ETag') + version1_etag = r.headers.get("ETag") # put a second version response, status = self.put( - self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # get first version again and confirm latest version has been updated - r = self.test_client.get(self.item_id_url + "?version=1", headers=[ - ('If-None-Match', version1_etag) - ]) + r = self.test_client.get( + self.item_id_url + "?version=1", headers=[("If-None-Match", version1_etag)] + ) document, status = self.parse_response(r) self.assert200(status) self.assertEqual(document[self.latest_version_field], 2) @@ -675,42 +716,39 @@ def test_automatic_fields(self): field manually. """ # set _version - self.item_change[self.version_field] = '1' - r, status = self.post( - self.known_resource_url, data=self.item_change) + self.item_change[self.version_field] = "1" + r, status = self.post(self.known_resource_url, data=self.item_change) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {self.version_field: 'unknown field'}) + self.assertValidationError(r, {self.version_field: "unknown field"}) # set _latest_version - self.item_change[self.latest_version_field] = '1' - r, status = self.post( - self.known_resource_url, data=self.item_change) + self.item_change[self.latest_version_field] = "1" + r, status = self.post(self.known_resource_url, data=self.item_change) self.assertValidationErrorStatus(status) - self.assertValidationError( - r, {self.latest_version_field: 'unknown field'}) + self.assertValidationError(r, {self.latest_version_field: "unknown field"}) # set _id_document - self.item_change[self.document_id_field] = '1' - r, status = self.post( - self.known_resource_url, data=self.item_change) + self.item_change[self.document_id_field] = "1" + r, status = self.post(self.known_resource_url, data=self.item_change) self.assertValidationErrorStatus(status) - self.assertValidationError( - r, {self.document_id_field: 'unknown field'}) + self.assertValidationError(r, {self.document_id_field: "unknown field"}) def test_referential_integrity(self): """ Make sure that Eve still correctly handles vanilla data_relations when versioning is turned on. (Copied from tests/methods/post.py.) """ data = {"person": self.unknown_item_id} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - expected = ("value '%s' must exist in resource '%s', field '%s'" % - (self.unknown_item_id, 'contacts', - self.id_field)) - self.assertValidationError(r, {'person': expected}) + expected = "value '%s' must exist in resource '%s', field '%s'" % ( + self.unknown_item_id, + "contacts", + self.id_field, + ) + self.assertValidationError(r, {"person": expected}) data = {"person": self.item_id} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) def test_delete(self): @@ -718,7 +756,7 @@ def test_delete(self): supposed to be versioned but whose shadow collection does not exist. """ # turn off filter setting - self.domain['contacts']['datasource']['filter'] = None + self.domain["contacts"]["datasource"]["filter"] = None # verify the primary collection exists but the shadow does not self.assertTrue(self.countDocuments() > 0) @@ -742,7 +780,8 @@ def test_deleteitem(self): # delete resource and verify no errors response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # verify that neither primary or shadow documents exist @@ -759,7 +798,8 @@ def test_softdelete(self): """ self.enableSoftDelete() response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # verify that the primary document and two (v1 and deleted v2) shadow @@ -793,14 +833,14 @@ def test_softdelete(self): r = self.test_client.get(self.item_id_url + "?version=all") document, status = self.parse_response(r) self.assert200(status) - items = document[self.app.config['ITEMS']] + items = document[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) self.assertEqual(items[1].get(self.deleted_field), True) r = self.test_client.get(self.item_id_url + "?version=diffs") document, status = self.parse_response(r) self.assert200(status) - items = document[self.app.config['ITEMS']] + items = document[self.app.config["ITEMS"]] self.assertEqual(len(items), 2) # Deleted item shoud diff by the version, etag, deleted, links, and # last_updated field only (the speed of test executon means @@ -809,11 +849,13 @@ def test_softdelete(self): changed_fields = [ self.version_field, self.deleted_field, - self.app.config['ETAG'], - self.app.config['LINKS']] + self.app.config["ETAG"], + self.app.config["LINKS"], + ] self.assertTrue( - len(items[1].keys()) == len(changed_fields) or - len(items[1].keys()) == len(changed_fields) + 1) + len(items[1].keys()) == len(changed_fields) + or len(items[1].keys()) == len(changed_fields) + 1 + ) for field in changed_fields: self.assertTrue(field in items[1], "%s not in diffs" % field) @@ -824,35 +866,33 @@ def test_softdelete_version_db_fields(self): self.enableSoftDelete() # v1 created before soft delete was enabled, it will not have DELETED - v1_doc = self._db[self.known_resource_shadow].find_one({ - self.document_id_field: ObjectId(self.item_id), - self.version_field: 1 - }) + v1_doc = self._db[self.known_resource_shadow].find_one( + {self.document_id_field: ObjectId(self.item_id), self.version_field: 1} + ) self.assertEqual(v1_doc.get(self.deleted_field), None) # Create second version r = self.test_client.patch( self.item_id_url, - data={'ref': '1234567890123456789012345'}, - headers=[('If-Match', self.item_etag)] + data={"ref": "1234567890123456789012345"}, + headers=[("If-Match", self.item_etag)], ) # Create deleted third version response, status = self.delete( - self.item_id_url, headers=[('If-Match', r.headers['ETag'])]) + self.item_id_url, headers=[("If-Match", r.headers["ETag"])] + ) self.assert204(status) # v2 doc should have DELETED = False added - v2_doc = self._db[self.known_resource_shadow].find_one({ - self.document_id_field: ObjectId(self.item_id), - self.version_field: 2 - }) + v2_doc = self._db[self.known_resource_shadow].find_one( + {self.document_id_field: ObjectId(self.item_id), self.version_field: 2} + ) self.assertEqual(v2_doc.get(self.deleted_field), False) # v3 should have DELETED = True - v3_doc = self._db[self.known_resource_shadow].find_one({ - self.document_id_field: ObjectId(self.item_id), - self.version_field: 3 - }) + v3_doc = self._db[self.known_resource_shadow].find_one( + {self.document_id_field: ObjectId(self.item_id), self.version_field: 3} + ) self.assertEqual(v3_doc.get(self.deleted_field), True) @@ -870,107 +910,121 @@ def test_referential_integrity(self): """ Make sure that Eve correctly validates a data_relation with a version and returns the version with the data_relation in the response. """ - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] - version_field = self.app.config['VERSION'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] + version_field = self.app.config["VERSION"] validation_error_format = ( "versioned data_relation must be a dict" - " with fields '%s' and '%s'" % (value_field, version_field)) + " with fields '%s' and '%s'" % (value_field, version_field) + ) # must be a dict data = {"person": self.item_id} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'person': 'must be of dict type'}) + self.assertValidationError(r, {"person": "must be of dict type"}) # must have _id data = {"person": {value_field: self.item_id}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'person': validation_error_format}) + self.assertValidationError(r, {"person": validation_error_format}) # must have _version data = {"person": {version_field: 1}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) - self.assertValidationError(r, {'person': validation_error_format}) + self.assertValidationError(r, {"person": validation_error_format}) # bad id format - data = {"person": {value_field: 'bad', version_field: 1}} - r, status = self.post('/invoices/', data=data) + data = {"person": {value_field: "bad", version_field: 1}} + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': {value_field: "must be of objectid type"}}) + r, {"person": {value_field: "must be of objectid type"}} + ) # unknown id - data = {"person": { - value_field: self.unknown_item_id, version_field: 1}} - r, status = self.post('/invoices/', data=data) + data = {"person": {value_field: self.unknown_item_id, version_field: 1}} + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': "value '%s' must exist in " - "resource '%s', field '%s' at version '%s'." % - (self.unknown_item_id, 'contacts', value_field, 1)}) + r, + { + "person": "value '%s' must exist in " + "resource '%s', field '%s' at version '%s'." + % (self.unknown_item_id, "contacts", value_field, 1) + }, + ) # version doesn't exist data = {"person": {value_field: self.item_id, version_field: 2}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': "value '%s' must exist in " - "resource '%s', field '%s' at version '%s'." % - (self.item_id, 'contacts', value_field, 2)}) + r, + { + "person": "value '%s' must exist in " + "resource '%s', field '%s' at version '%s'." + % (self.item_id, "contacts", value_field, 2) + }, + ) # put a second version - response, status = self.put(self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # reference first version... this should work data = {"person": {value_field: self.item_id, version_field: 1}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) # and response should include embedded v1 response, status = self.get( - self.domain['invoices']['url'], + self.domain["invoices"]["url"], item=r[self.id_field], - query='?embedded={"person": 1}') + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertEqual(response['person'].get(version_field), 1) + self.assertEqual(response["person"].get(version_field), 1) # reference second version... this should work data = {"person": {value_field: self.item_id, version_field: 2}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assert201(status) # and response should include embedded v2 response, status = self.get( - self.domain['invoices']['url'], + self.domain["invoices"]["url"], item=r[self.id_field], - query='?embedded={"person": 1}') + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertEqual(response['person'].get(version_field), 2) + self.assertEqual(response["person"].get(version_field), 2) def test_embedded(self): """ Perform a quick check to make sure that Eve can embedded with a version in the data relation. """ - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] # add embeddable data relation data = {"person": {value_field: self.item_id, self.version_field: 1}} - response, status = self.post('/invoices/', data=data) + response, status = self.post("/invoices/", data=data) self.assert201(status) invoice_id = response[value_field] # test that it works response, status = self.get( - self.domain['invoices']['url'], - item=invoice_id, query='?embedded={"person": 1}') + self.domain["invoices"]["url"], + item=invoice_id, + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertTrue('ref' in response['person']) + self.assertTrue("ref" in response["person"]) def test_softdelete_embedded(self): """ If a versioned embedded document is soft deleted, a previous @@ -978,33 +1032,36 @@ def test_softdelete_embedded(self): """ self.enableSoftDelete() - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] - version_field = self.app.config['VERSION'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] + version_field = self.app.config["VERSION"] # add embeddable data relation data = {"person": {value_field: self.item_id, version_field: 1}} - response, status = self.post('/invoices/', data=data) + response, status = self.post("/invoices/", data=data) self.assert201(status) invoice_id = response[value_field] # soft delete embedded doc response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # v1 should still return response, status = self.get( - self.domain['invoices']['url'], - item=invoice_id, query='?embedded={"person": 1}') + self.domain["invoices"]["url"], + item=invoice_id, + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertEqual(response['person'].get(self.id_field), self.item_id) - self.assertEqual(response['person'].get( - self.app.config['ETAG']), self.item_etag) - self.assertEqual(response['person'].get(self.version_field), 1) - self.assertEqual(response['person'].get(self.deleted_field), False) + self.assertEqual(response["person"].get(self.id_field), self.item_id) + self.assertEqual( + response["person"].get(self.app.config["ETAG"]), self.item_etag + ) + self.assertEqual(response["person"].get(self.version_field), 1) + self.assertEqual(response["person"].get(self.deleted_field), False) def test_softdelete_data_relation_validation(self): """Eve validation should not allow a data relation to a soft deleted @@ -1016,27 +1073,31 @@ def test_softdelete_data_relation_validation(self): # soft delete embeddable document self.enableSoftDelete() response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # creating data relation to still valid v1 should work - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] - version_field = self.app.config['VERSION'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] + version_field = self.app.config["VERSION"] data = {"person": {value_field: self.item_id, version_field: 1}} - response, status = self.post('/invoices/', data=data) + response, status = self.post("/invoices/", data=data) self.assert201(status) # saving relation to deleted version 2 should fail data = {"person": {value_field: self.item_id, version_field: 2}} - r, status = self.post('/invoices/', data=data) + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': "value '%s' must exist in " - "resource '%s', field '%s' at version '%s'." % - (self.item_id, 'contacts', value_field, 2)}) + r, + { + "person": "value '%s' must exist in " + "resource '%s', field '%s' at version '%s'." + % (self.item_id, "contacts", value_field, 2) + }, + ) class TestVersionedDataRelationCustomField(TestNormalVersioning): @@ -1055,30 +1116,38 @@ def test_referential_integrity(self): referencing fields that aren't '_id'. """ # put a second version - response, status = self.put(self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # try saving a field from the first version against version 2 - data = {"person": {'ref': self.item['ref'], self.version_field: 2}} - r, status = self.post('/invoices/', data=data) + data = {"person": {"ref": self.item["ref"], self.version_field: 2}} + r, status = self.post("/invoices/", data=data) self.assertValidationErrorStatus(status) self.assertValidationError( - r, {'person': "value '%s' must exist in " - "resource '%s', field '%s' at version '%s'." % - (self.item['ref'], 'contacts', 'ref', 2)}) + r, + { + "person": "value '%s' must exist in " + "resource '%s', field '%s' at version '%s'." + % (self.item["ref"], "contacts", "ref", 2) + }, + ) # try saving against the first version...this should work - data = {"person": {'ref': self.item['ref'], self.version_field: 1}} - r, status = self.post('/invoices/', data=data) + data = {"person": {"ref": self.item["ref"], self.version_field: 1}} + r, status = self.post("/invoices/", data=data) self.assert201(status) # and response should include embedded v1 response, status = self.get( - self.domain['invoices']['url'], + self.domain["invoices"]["url"], item=r[self.id_field], - query='?embedded={"person": 1}') + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertEqual(response['person'].get(self.version_field), 1) + self.assertEqual(response["person"].get(self.version_field), 1) class TestVersionedDataRelationUnversionedField(TestNormalVersioning): @@ -1088,7 +1157,8 @@ def setUp(self): # enable versioning in the invoice data_relation definition with custom # unversioned relation field self.enableDataVersionRelation( - custom_field=self.unversioned_field, custom_field_type='integer') + custom_field=self.unversioned_field, custom_field_type="integer" + ) self.enableVersioning(partial=True) self.insertTestData() @@ -1098,25 +1168,31 @@ def test_referential_integrity(self): referencing unversioned fields """ # put a second version - response, status = self.put(self.item_id_url, data=self.item_change, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, + data=self.item_change, + headers=[("If-Match", self.item_etag)], + ) self.assertGoodPutPatch(response, status) # reference first version relation_field = self.unversioned_field - data = {"person": { - relation_field: self.item_change[relation_field], - self.version_field: 1 - }} - r, status = self.post('/invoices/', data=data) + data = { + "person": { + relation_field: self.item_change[relation_field], + self.version_field: 1, + } + } + r, status = self.post("/invoices/", data=data) self.assert201(status) # and response should include embedded v1 response, status = self.get( - self.domain['invoices']['url'], + self.domain["invoices"]["url"], item=r[self.id_field], - query='?embedded={"person": 1}') + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertEqual(response['person'].get(self.version_field), 1) + self.assertEqual(response["person"].get(self.version_field), 1) class TestPartialVersioning(TestNormalVersioning): @@ -1185,8 +1261,8 @@ def test_get(self): """ response, status = self.get(self.known_resource) self.assert200(status) - items = response[self.app.config['ITEMS']] - self.assertEqual(len(items), self.app.config['PAGINATION_DEFAULT']) + items = response[self.app.config["ITEMS"]] + self.assertEqual(len(items), self.app.config["PAGINATION_DEFAULT"]) for item in items: self.assertDocumentVersionFields(item, 1) @@ -1208,8 +1284,9 @@ def test_put(self): # put a change changes = {"ref": "this is a different value"} - response, status = self.put(self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + response, status = self.put( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assertGoodPutPatch(response, status) self.assertDocumentVersionFields(response, 2) @@ -1231,8 +1308,8 @@ def test_patch(self): # patch a change changes = {"ref": "this is a different value"} response, status = self.patch( - self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assertGoodPutPatch(response, status) self.assertDocumentVersionFields(response, 2) @@ -1252,8 +1329,8 @@ def test_datasource(self): # patch a change changes = {"ref": "this is a different value"} response, status = self.patch( - self.item_id_url, data=changes, - headers=[('If-Match', self.item_etag)]) + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) self.assertGoodPutPatch(response, status) self.assertDocumentVersionFields(response, 2) @@ -1261,8 +1338,8 @@ def test_datasource(self): self.assertTrue(self.countShadowDocuments() == 2) data = { - self.versioned_field: 'ref value 3..............', - self.unversioned_field: 444 + self.versioned_field: "ref value 3..............", + self.unversioned_field: 444, } contact, status = self.post(self.known_resource_url, data=data) self.assert201(status) @@ -1275,7 +1352,7 @@ def test_delete(self): supposed to be versioned but whose shadow collection does not exist. """ # turn off filter setting - self.domain['contacts']['datasource']['filter'] = None + self.domain["contacts"]["datasource"]["filter"] = None # verify the primary collection exists but the shadow does not self.assertTrue(self.countDocuments() > 0) @@ -1299,7 +1376,8 @@ def test_deleteitem(self): # delete resource and verify no errors response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # verify that neither primary or shadow documents exist @@ -1319,7 +1397,8 @@ def test_softdelete(self): # soft delete resource and verify no errors response, status = self.delete( - self.item_id_url, headers=[('If-Match', self.item_etag)]) + self.item_id_url, headers=[("If-Match", self.item_etag)] + ) self.assert204(status) # verify that the primary document and two (late caught v1 and deleted @@ -1331,45 +1410,43 @@ def test_referential_integrity(self): """ Make sure that Eve doesn't mind doing a data relation even when the shadow copy doesn't exist. """ - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] - version_field = self.app.config['VERSION'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] + version_field = self.app.config["VERSION"] # verify that Eve will take version = 1 if no shadow docs exist data = {"person": {value_field: self.item_id, version_field: 1}} - response, status = self.post('/invoices/', data=data) + response, status = self.post("/invoices/", data=data) self.assert201(status) def test_embedded(self): """ Perform a quick check to make sure that Eve can embedded with a version in the data relation. """ - data_relation = \ - self.domain['invoices']['schema']['person']['data_relation'] - value_field = data_relation['field'] - version_field = self.app.config['VERSION'] + data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] + value_field = data_relation["field"] + version_field = self.app.config["VERSION"] # verify that Eve will take version = 1 if no shadow docs exist data = {"person": {value_field: self.item_id, version_field: 1}} - response, status = self.post('/invoices/', data=data) + response, status = self.post("/invoices/", data=data) self.assert201(status) invoice_id = response[value_field] # verify that we can embed across the data_relation w/o shadow copy response, status = self.get( - self.domain['invoices']['url'], - item=invoice_id, query='?embedded={"person": 1}') + self.domain["invoices"]["url"], + item=invoice_id, + query='?embedded={"person": 1}', + ) self.assert200(status) - self.assertTrue('ref' in response['person']) + self.assertTrue("ref" in response["person"]) class TestVersioningWithCustomIdField(TestNormalVersioning): def setUp(self): super(TestVersioningWithCustomIdField, self).setUp() - self.domain[self.known_resource]['schema'][self.id_field] = { - 'type': 'string', - } + self.domain[self.known_resource]["schema"][self.id_field] = {"type": "string"} self.enableVersioning() self.insertTestData() diff --git a/eve/utils.py b/eve/utils.py index d469b0e03..8db19ac3c 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -31,6 +31,7 @@ class Config(object): setting in the eve __init__.py module, otherwise returns the flaskapp config value (which value might override the static defaults). """ + def __getattr__(self, name): try: # will return 'working outside of application context' if the @@ -58,6 +59,7 @@ class ParsedRequest(object): .. versionchanged:: 0.0.6 Projection queries ('?projection={"name": 1}') """ + # `where` value of the query string (?where). Defaults to None. where = None @@ -126,29 +128,27 @@ def parse_request(resource): r.args = args settings = config.DOMAIN[resource] - if settings['allowed_filters']: + if settings["allowed_filters"]: r.where = args.get(config.QUERY_WHERE) - if settings['projection']: + if settings["projection"]: r.projection = args.get(config.QUERY_PROJECTION) - if settings['sorting']: + if settings["sorting"]: r.sort = args.get(config.QUERY_SORT) - if settings['embedding']: + if settings["embedding"]: r.embedded = args.get(config.QUERY_EMBEDDED) - if settings['datasource']['aggregation']: + if settings["datasource"]["aggregation"]: r.aggregation = args.get(config.QUERY_AGGREGATION) r.show_deleted = config.SHOW_DELETED_PARAM in args - max_results_default = config.PAGINATION_DEFAULT if \ - settings['pagination'] else 0 + max_results_default = config.PAGINATION_DEFAULT if settings["pagination"] else 0 try: r.max_results = int(float(args[config.QUERY_MAX_RESULTS])) assert r.max_results > 0 - except (ValueError, werkzeug.exceptions.BadRequestKeyError, - AssertionError): + except (ValueError, werkzeug.exceptions.BadRequestKeyError, AssertionError): r.max_results = max_results_default - if settings['pagination']: + if settings["pagination"]: # TODO should probably return a 400 if 'page' is < 1 or non-numeric if config.QUERY_PAGE in args: try: @@ -165,18 +165,18 @@ def etag_parse(challenge): if challenge in headers: etag = headers[challenge] # allow weak etags (Eve does not support byte-range requests) - if etag.startswith('W/\"'): - etag = etag.lstrip('W/') + if etag.startswith('W/"'): + etag = etag.lstrip("W/") # remove double quotes from challenge etag format to allow direct # string comparison with stored values - return etag.replace('\"', '') + return etag.replace('"', "") else: return None if headers: - r.if_modified_since = weak_date(headers.get('If-Modified-Since')) - r.if_none_match = etag_parse('If-None-Match') - r.if_match = etag_parse('If-Match') + r.if_modified_since = weak_date(headers.get("If-Modified-Since")) + r.if_none_match = etag_parse("If-None-Match") + r.if_match = etag_parse("If-Match") return r @@ -189,8 +189,11 @@ def weak_date(date): :param date: the date to be adjusted. """ - return datetime.strptime(date, RFC1123_DATE_FORMAT) + \ - timedelta(seconds=1) if date else None + return ( + datetime.strptime(date, RFC1123_DATE_FORMAT) + timedelta(seconds=1) + if date + else None + ) def str_to_date(string): @@ -227,7 +230,7 @@ def home_link(): .. versionchanged:: 0.0.3 Now returning a JSON link. """ - return {'title': 'home', 'href': '/'} + return {"title": "home", "href": "/"} def api_prefix(url_prefix=None, api_version=None): @@ -253,13 +256,19 @@ def api_prefix(url_prefix=None, api_version=None): if api_version is None: api_version = config.API_VERSION - prefix = '/%s' % url_prefix if url_prefix else '' - version = '/%s' % api_version if api_version else '' + prefix = "/%s" % url_prefix if url_prefix else "" + version = "/%s" % api_version if api_version else "" return prefix + version -def querydef(max_results=config.PAGINATION_DEFAULT, where=None, sort=None, - version=None, page=None, other_params=MultiDict()): +def querydef( + max_results=config.PAGINATION_DEFAULT, + where=None, + sort=None, + version=None, + page=None, + other_params=MultiDict(), +): """ Returns a valid query string. :param max_results: `max_result` part of the query string. Defaults to @@ -275,25 +284,42 @@ def querydef(max_results=config.PAGINATION_DEFAULT, where=None, sort=None, Support for customizable query parameters. Add version to query string (#475). """ - where_part = '&%s=%s' % (config.QUERY_WHERE, where) if where else '' - sort_part = '&%s=%s' % (config.QUERY_SORT, sort) if sort else '' - page_part = '&%s=%s' % (config.QUERY_PAGE, page) if page and page > 1 \ - else '' - version_part = '&%s=%s' % (config.VERSION_PARAM, version) if version \ - else '' - max_results_part = '%s=%s' % (config.QUERY_MAX_RESULTS, max_results) \ - if max_results != config.PAGINATION_DEFAULT else '' - other_params_part = ''.join('&%s=%s' % (param, value) for param, values - in other_params.lists() for value in values) + where_part = "&%s=%s" % (config.QUERY_WHERE, where) if where else "" + sort_part = "&%s=%s" % (config.QUERY_SORT, sort) if sort else "" + page_part = "&%s=%s" % (config.QUERY_PAGE, page) if page and page > 1 else "" + version_part = "&%s=%s" % (config.VERSION_PARAM, version) if version else "" + max_results_part = ( + "%s=%s" % (config.QUERY_MAX_RESULTS, max_results) + if max_results != config.PAGINATION_DEFAULT + else "" + ) + other_params_part = "".join( + "&%s=%s" % (param, value) + for param, values in other_params.lists() + for value in values + ) # remove sort set by Eve if version is set if version and sort is not None: - sort_part = '&%s=%s' % (config.QUERY_SORT, sort) \ - if sort != '[("%s", 1)]' % config.VERSION else '' - - return ('?' + ''.join([max_results_part, where_part, sort_part, - version_part, page_part, other_params_part]) - .lstrip('&')).rstrip('?') + sort_part = ( + "&%s=%s" % (config.QUERY_SORT, sort) + if sort != '[("%s", 1)]' % config.VERSION + else "" + ) + + return ( + "?" + + "".join( + [ + max_results_part, + where_part, + sort_part, + version_part, + page_part, + other_params_part, + ] + ).lstrip("&") + ).rstrip("?") def document_etag(value, ignore_fields=None): @@ -311,6 +337,7 @@ def document_etag(value, ignore_fields=None): consistent between different runs and/or server instances (#16). """ if ignore_fields: + def filter_ignore_fields(d, fields): # recursive function to remove the fields that they are in d, # field is a list of fields to skip or dotted fields to look up @@ -332,8 +359,9 @@ def filter_ignore_fields(d, fields): h = hashlib.sha1() json_encoder = app.data.json_encoder_class() - h.update(dumps(value_, sort_keys=True, - default=json_encoder.default).encode('utf-8')) + h.update( + dumps(value_, sort_keys=True, default=json_encoder.default).encode("utf-8") + ) return h.hexdigest() @@ -362,7 +390,7 @@ def debug_error_message(msg): .. versionadded: 0.0.9 """ - if getattr(config, 'DEBUG', False): + if getattr(config, "DEBUG", False): return msg return None @@ -380,52 +408,54 @@ def validate_filters(where, resource): .. versionadded: 0.0.9 """ - operators = getattr(app.data, 'operators', set()) - allowed = config.DOMAIN[resource]['allowed_filters'] + list(operators) + operators = getattr(app.data, "operators", set()) + allowed = config.DOMAIN[resource]["allowed_filters"] + list(operators) def validate_filter(filter): for key, value in filter.items(): - if '*' not in allowed: + if "*" not in allowed: + def recursive_check_allowed(filter_key, allowed_filters): if filter_key not in allowed_filters: - base_composed_key, _, _ = filter_key.rpartition('.') + base_composed_key, _, _ = filter_key.rpartition(".") return base_composed_key and recursive_check_allowed( - base_composed_key, allowed_filters) + base_composed_key, allowed_filters + ) return True if not recursive_check_allowed(key, allowed): return "filter on '%s' not allowed" % key - if key in ('$or', '$and', '$nor'): + if key in ("$or", "$and", "$nor"): if not isinstance(value, list): return "operator '%s' expects a list of sub-queries" % key for v in value: if not isinstance(v, dict): - return "operator '%s' expects a list of sub-queries" \ - % key + return "operator '%s' expects a list of sub-queries" % key r = validate_filter(v) if r: return r else: if config.VALIDATE_FILTERS: + def get_sub_schemas(base_schema): def dict_sub_schema(base): - if base.get('type') == 'dict': - return base.get('schema') + if base.get("type") == "dict": + return base.get("schema") return None - if base_schema.get('type') == 'list': - if 'schema' in base_schema: + if base_schema.get("type") == "list": + if "schema" in base_schema: # Try to get dict sub-schema for arbitrary # sized list - sub = dict_sub_schema(base_schema['schema']) + sub = dict_sub_schema(base_schema["schema"]) return [sub] if sub is not None else [] - elif 'items' in base_schema: + elif "items" in base_schema: # Try to get dict sub-schema(s) for # fixed-size list - items = base_schema['items'] + items = base_schema["items"] sub_schemas = [] for item in items: sub = dict_sub_schema(item) @@ -439,15 +469,15 @@ def dict_sub_schema(base): def recursive_validate_filter(key, value, schema): if key not in schema: - base_key, _, sub_keys = key.partition('.') + base_key, _, sub_keys = key.partition(".") if sub_keys and base_key in schema: # key is the composition of base field and # sub-fields sub_schemas = get_sub_schemas(schema[base_key]) for sub_schema in sub_schemas: - if recursive_validate_filter(sub_keys, - value, - sub_schema): + if recursive_validate_filter( + sub_keys, value, sub_schema + ): return True return False @@ -456,13 +486,13 @@ def recursive_validate_filter(key, value, schema): v = app.validator({key: field_schema}) return v.validate({key: value}) - res_schema = config.DOMAIN[resource]['schema'] + res_schema = config.DOMAIN[resource]["schema"] if not recursive_validate_filter(key, value, res_schema): return "filter on '%s' is invalid" % key return None - if '*' in allowed and not config.VALIDATE_FILTERS: + if "*" in allowed and not config.VALIDATE_FILTERS: return None return validate_filter(where) @@ -481,18 +511,22 @@ def auto_fields(resource): resource_def = config.DOMAIN[resource] # preserved meta data - fields = [resource_def['id_field'], config.LAST_UPDATED, - config.DATE_CREATED, config.ETAG] + fields = [ + resource_def["id_field"], + config.LAST_UPDATED, + config.DATE_CREATED, + config.ETAG, + ] # on-the-fly meta data (not in data store) fields += [config.ISSUES, config.STATUS, config.LINKS] - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: fields.append(config.VERSION) fields.append(config.LATEST_VERSION) # on-the-fly meta data - fields.append(resource_def['id_field'] + config.VERSION_ID_SUFFIX) + fields.append(resource_def["id_field"] + config.VERSION_ID_SUFFIX) - if resource_def['soft_delete'] is True: + if resource_def["soft_delete"] is True: fields.append(config.DELETED) return fields @@ -507,8 +541,8 @@ def import_from_string(module_name): """ try: - modules = module_name.split('.') - module_path, attr = '.'.join(modules[:-1]), modules[-1] + modules = module_name.split(".") + module_path, attr = ".".join(modules[:-1]), modules[-1] return getattr(import_module(module_path), attr) except (ImportError, AttributeError): - raise ImportError('Cannot import {}'.format(module_name)) + raise ImportError("Cannot import {}".format(module_name)) diff --git a/eve/validation.py b/eve/validation.py index 0d5c5016e..48b7fc3e1 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -21,15 +21,14 @@ class Validator(cerberus.Validator): - def __init__(self, *args, **kwargs): if not config.VALIDATION_ERROR_AS_LIST: - kwargs['error_handler'] = SingleErrorAsStringErrorHandler + kwargs["error_handler"] = SingleErrorAsStringErrorHandler - resource = kwargs.get('resource', None) + resource = kwargs.get("resource", None) if resource: resource_def = config.DOMAIN[resource] - kwargs['allow_unknown'] = resource_def['allow_unknown'] + kwargs["allow_unknown"] = resource_def["allow_unknown"] super(Validator, self).__init__(*args, **kwargs) def validate_update(self, document, document_id, persisted_document=None): @@ -64,8 +63,7 @@ def validate_replace(self, document, document_id, persisted_document=None): def _normalize_default(self, mapping, schema, field): """ {'nullable': True} """ - if not self.persisted_document or \ - field not in self.persisted_document: + if not self.persisted_document or field not in self.persisted_document: super(Validator, self)._normalize_default(mapping, schema, field) def _normalize_default_setter(self, mapping, schema, field): @@ -73,10 +71,8 @@ def _normalize_default_setter(self, mapping, schema, field): {'type': 'callable'}, {'type': 'string'} ]} """ - if not self.persisted_document or \ - field not in self.persisted_document: - super(Validator, self)._normalize_default_setter(mapping, schema, - field) + if not self.persisted_document or field not in self.persisted_document: + super(Validator, self)._normalize_default_setter(mapping, schema, field) def _validate_dependencies(self, dependencies, field, value): """ {'type': ['dict', 'hashable', 'list']} """ @@ -89,47 +85,49 @@ def _validate_dependencies(self, dependencies, field, value): validator.validate(dcopy, update=self.update) self._error(validator._errors) else: - super(Validator, self)._validate_dependencies(dependencies, field, - value) + super(Validator, self)._validate_dependencies(dependencies, field, value) def _filter_persisted_fields_not_in_document(self, fields): def persisted_but_not_in_document(field): - return field not in self.document and \ - self.persisted_document and \ - field in self.persisted_document - return [field for field in fields if - persisted_but_not_in_document(field)] + return ( + field not in self.document + and self.persisted_document + and field in self.persisted_document + ) + + return [field for field in fields if persisted_but_not_in_document(field)] def _validate_readonly(self, read_only, field, value): """ {'type': 'boolean'} """ - persisted_value = self.persisted_document.get(field) \ - if self.persisted_document else None + persisted_value = ( + self.persisted_document.get(field) if self.persisted_document else None + ) if value != persisted_value: super(Validator, self)._validate_readonly(read_only, field, value) @property def resource(self): - return self._config.get('resource', None) + return self._config.get("resource", None) @resource.setter def resource(self, value): - self._config['resource'] = value + self._config["resource"] = value @property def document_id(self): - return self._config.get('document_id', None) + return self._config.get("document_id", None) @document_id.setter def document_id(self, value): - self._config['document_id'] = value + self._config["document_id"] = value @property def persisted_document(self): - return self._config.get('persisted_document', None) + return self._config.get("persisted_document", None) @persisted_document.setter def persisted_document(self, value): - self._config['persisted_document'] = value + self._config["persisted_document"] = value class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler): diff --git a/eve/versioning.py b/eve/versioning.py index 740d750f5..ca9ab8437 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -8,7 +8,7 @@ def versioned_id_field(resource_settings): .. versionadded: 0.4 """ - return resource_settings['id_field'] + app.config['VERSION_ID_SUFFIX'] + return resource_settings["id_field"] + app.config["VERSION_ID_SUFFIX"] def resolve_document_version(document, resource, method, latest_doc=None): @@ -21,14 +21,14 @@ def resolve_document_version(document, resource, method, latest_doc=None): .. versionadded:: 0.4 """ - resource_def = app.config['DOMAIN'][resource] - version = app.config['VERSION'] - latest_version = app.config['LATEST_VERSION'] + resource_def = app.config["DOMAIN"][resource] + version = app.config["VERSION"] + latest_version = app.config["LATEST_VERSION"] - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: # especially on collection endpoints, we don't to ensure an extra # lookup if we are already pulling the latest version - if method == 'GET' and latest_doc is None: + if method == "GET" and latest_doc is None: if version not in document: # well it should be... the api designer must have turned on # versioning after data was already in the collection or the @@ -38,7 +38,7 @@ def resolve_document_version(document, resource, method, latest_doc=None): # include latest_doc if the request is for an older version so that we # can set the latest_version field in the response - if method == 'GET' and latest_doc is not None: + if method == "GET" and latest_doc is not None: if version not in latest_doc: # well it should be... the api designer must have turned on # versioning after data was already in the collection or the @@ -52,16 +52,20 @@ def resolve_document_version(document, resource, method, latest_doc=None): # was turned on or outside of Eve document[version] = 1 - if method == 'POST': + if method == "POST": # this one is easy! it is a new document document[version] = 1 - if method == 'PUT' or method == 'PATCH' or \ - (method == 'DELETE' and resource_def['soft_delete'] is True): + if ( + method == "PUT" + or method == "PATCH" + or (method == "DELETE" and resource_def["soft_delete"] is True) + ): if not latest_doc: - abort(500, description=debug_error_message( - 'I need the latest document here!' - )) + abort( + 500, + description=debug_error_message("I need the latest document here!"), + ) if version in latest_doc: # all is right in the world :) document[version] = latest_doc[version] + 1 @@ -83,10 +87,10 @@ def late_versioning_catch(document, resource): .. versionadded:: 0.4 """ - resource_def = app.config['DOMAIN'][resource] - version = app.config['VERSION'] + resource_def = app.config["DOMAIN"][resource] + version = app.config["VERSION"] - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: # TODO: Could directly check that there are no shadow copies for this # document. If there are shadow copies but the version field is in the # stored document, then something is wrong. (Modified outside of Eve?) @@ -95,7 +99,7 @@ def late_versioning_catch(document, resource): # The API maintainer must of turned on versioning after the # document was added to the database, so let's add this old version # to the shadow collection now as if it was a new document. - resolve_document_version(document, resource, 'POST') + resolve_document_version(document, resource, "POST") insert_versioning_documents(resource, document) @@ -107,13 +111,13 @@ def insert_versioning_documents(resource, documents): .. versionadded:: 0.4 """ - resource_def = app.config['DOMAIN'][resource] - _id = resource_def['id_field'] + resource_def = app.config["DOMAIN"][resource] + _id = resource_def["id_field"] # push back versioned items if applicable # note: MongoDB doesn't have transactions! if the server dies, no # history will be saved. - if resource_def['versioning'] is True: + if resource_def["versioning"] is True: # force input as lists if not isinstance(documents, list): documents = [documents] @@ -121,13 +125,13 @@ def insert_versioning_documents(resource, documents): # if 'user-restricted resource access' is enabled and there's # an Auth request active, inject the username into the document request_auth_value = None - auth = resource_def['authentication'] - auth_field = resource_def['auth_field'] + auth = resource_def["authentication"] + auth_field = resource_def["auth_field"] if auth and auth_field: request_auth_value = auth.get_request_auth_value() # build vesioning documents - version = app.config['VERSION'] + version = app.config["VERSION"] versioned_documents = [] for index, document in enumerate(documents): ver_doc = {} @@ -150,8 +154,8 @@ def insert_versioning_documents(resource, documents): versioned_documents.append(ver_doc) # bulk insert - source = resource_def['datasource']['source'] - versionable_resource_name = source + app.config['VERSIONS'] + source = resource_def["datasource"]["source"] + versionable_resource_name = source + app.config["VERSIONS"] app.data.insert(versionable_resource_name, versioned_documents) @@ -168,19 +172,20 @@ def versioned_fields(resource_def): .. versionadded:: 0.4 """ - if resource_def['versioning'] is not True: + if resource_def["versioning"] is not True: return [] - schema = resource_def['schema'] + schema = resource_def["schema"] - fields = [f for f in schema - if schema[f].get('versioned', True) is True and - f != resource_def['id_field']] + fields = [ + f + for f in schema + if schema[f].get("versioned", True) is True and f != resource_def["id_field"] + ] - fields.extend((app.config['LAST_UPDATED'], - app.config['ETAG'], - app.config['DELETED'], - )) + fields.extend( + (app.config["LAST_UPDATED"], app.config["ETAG"], app.config["DELETED"]) + ) return fields @@ -195,25 +200,27 @@ def diff_document(resource_def, old_doc, new_doc): .. versionadded:: 0.4 """ diff = {} - fields = list(resource_def['schema'].keys()) + [ - app.config['VERSION'], - app.config['LATEST_VERSION'], - resource_def['id_field'], - app.config['LAST_UPDATED'], - app.config['DATE_CREATED'], - app.config['ETAG'], - app.config['LINKS']] - if resource_def['soft_delete'] is True: - fields.append(app.config['DELETED']) + fields = list(resource_def["schema"].keys()) + [ + app.config["VERSION"], + app.config["LATEST_VERSION"], + resource_def["id_field"], + app.config["LAST_UPDATED"], + app.config["DATE_CREATED"], + app.config["ETAG"], + app.config["LINKS"], + ] + if resource_def["soft_delete"] is True: + fields.append(app.config["DELETED"]) for field in fields: - if field in new_doc and \ - (field not in old_doc or new_doc[field] != old_doc[field]): + if field in new_doc and ( + field not in old_doc or new_doc[field] != old_doc[field] + ): diff[field] = new_doc[field] # This method does not show when fields are deleted. - for field in app.config['VERSION_DIFF_INCLUDE']: + for field in app.config["VERSION_DIFF_INCLUDE"]: if field in new_doc: diff[field] = new_doc[field] @@ -241,11 +248,13 @@ def synthesize_versioned_document(document, delta, resource_def): id_field = versioned_id_field(resource_def) if id_field not in delta: - abort(400, description=debug_error_message( - 'You must include %s in any projection with a version query.' - % id_field - )) - delta[resource_def['id_field']] = delta[id_field] + abort( + 400, + description=debug_error_message( + "You must include %s in any projection with a version query." % id_field + ), + ) + delta[resource_def["id_field"]] = delta[id_field] del delta[id_field] # add unversioned fields from latest document to versioned_doc @@ -276,29 +285,30 @@ def get_old_document(resource, req, lookup, document, version): .. versionadded:: 0.4 """ - if version != 'all' and version != 'diffs' and version is not None: + if version != "all" and version != "diffs" and version is not None: try: version = int(version) assert version > 0 except (ValueError, BadRequestKeyError, AssertionError): - abort(400, description=debug_error_message( - 'Document version number should be an int greater than 0' - )) + abort( + 400, + description=debug_error_message( + "Document version number should be an int greater than 0" + ), + ) # parameters to find specific document version resource_def = config.DOMAIN[resource] if versioned_id_field(resource_def) not in lookup: - lookup[versioned_id_field(resource_def)] \ - = lookup[resource_def['id_field']] - del lookup[resource_def['id_field']] + lookup[versioned_id_field(resource_def)] = lookup[resource_def["id_field"]] + del lookup[resource_def["id_field"]] lookup[config.VERSION] = version # synthesize old document from latest and delta delta = app.data.find_one(resource + config.VERSIONS, req, **lookup) if not delta: abort(404) - old_document = synthesize_versioned_document( - document, delta, resource_def) + old_document = synthesize_versioned_document(document, delta, resource_def) else: # perform a shallow copy to allow this document to be used as a delta # for synthesize_versioned_document where id_field is removed @@ -318,12 +328,12 @@ def get_data_version_relation_document(data_relation, reference, latest=False): .. versionadded:: 0.4 """ - value_field = data_relation['field'] - version_field = app.config['VERSION'] - collection = data_relation['resource'] + value_field = data_relation["field"] + version_field = app.config["VERSION"] + collection = data_relation["resource"] versioned_collection = collection + config.VERSIONS - resource_def = app.config['DOMAIN'][data_relation['resource']] - id_field = resource_def['id_field'] + resource_def = app.config["DOMAIN"][data_relation["resource"]] + id_field = resource_def["id_field"] # Fetch document data at the referenced version query = {version_field: reference[version_field]} @@ -334,10 +344,11 @@ def get_data_version_relation_document(data_relation, reference, latest=False): # The relation value field is unversioned, and will not be present in # the versioned collection. Need to find id field for version query req = ParsedRequest() - if resource_def['soft_delete']: + if resource_def["soft_delete"]: req.show_deleted = True latest_version = app.data.find_one( - collection, req, **{value_field: reference[value_field]}) + collection, req, **{value_field: reference[value_field]} + ) if not latest_version: return None query[versioned_id_field(resource_def)] = latest_version[id_field] @@ -360,7 +371,7 @@ def get_data_version_relation_document(data_relation, reference, latest=False): # Fetch the latest version of this document to use in version synthesis query = {id_field: referenced_version[versioned_id_field(resource_def)]} req = ParsedRequest() - if resource_def['soft_delete']: + if resource_def["soft_delete"]: # Still return latest after soft delete. It is needed to synthesize # full document version. req.show_deleted = True @@ -370,7 +381,8 @@ def get_data_version_relation_document(data_relation, reference, latest=False): # Syntheisze referenced version from latest and versioned data document = synthesize_versioned_document( - latest_version, referenced_version, resource_def) + latest_version, referenced_version, resource_def + ) return document @@ -384,11 +396,11 @@ def missing_version_field(data_relation, reference): .. versionadded:: 0.4 """ - value_field = data_relation['field'] - version_field = app.config['VERSION'] - collection = data_relation['resource'] + value_field = data_relation["field"] + version_field = app.config["VERSION"] + collection = data_relation["resource"] query = {} query[value_field] = reference[value_field] - query[version_field] = {'$exists': False} + query[version_field] = {"$exists": False} return app.data.find_one(collection, None, **query) diff --git a/examples/notifications.py b/examples/notifications.py index bdccd774f..00b92e426 100644 --- a/examples/notifications.py +++ b/examples/notifications.py @@ -26,7 +26,7 @@ @app.before_request def before(): - print('the request object ready to be processed:', request) + print("the request object ready to be processed:", request) @app.after_request @@ -35,8 +35,9 @@ def after(response): Your function must take one parameter, a `response_class` object and return a new response object or the same (see Flask documentation). """ - print('and here we have the response object instead:', response) + print("and here we have the response object instead:", response) return response -if __name__ == '__main__': + +if __name__ == "__main__": app.run() diff --git a/examples/notifications_settings.py b/examples/notifications_settings.py index d559518d2..7d072e3b8 100644 --- a/examples/notifications_settings.py +++ b/examples/notifications_settings.py @@ -1,5 +1,2 @@ # -*- coding: utf-8 -*- -SETTINGS = { - 'DEBUG': True, - 'DOMAIN': {'test': {}} -} +SETTINGS = {"DEBUG": True, "DOMAIN": {"test": {}}} diff --git a/examples/security/bcrypt.py b/examples/security/bcrypt.py index 671d22691..46e60706c 100644 --- a/examples/security/bcrypt.py +++ b/examples/security/bcrypt.py @@ -29,12 +29,14 @@ class BCryptAuth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): # use Eve's own db driver; no additional connections/resources are used - accounts = app.data.driver.db['accounts'] - account = accounts.find_one({'username': username}) - return account and \ - bcrypt.hashpw(password, account['password']) == account['password'] + accounts = app.data.driver.db["accounts"] + account = accounts.find_one({"username": username}) + return ( + account + and bcrypt.hashpw(password, account["password"]) == account["password"] + ) -if __name__ == '__main__': +if __name__ == "__main__": app = Eve(auth=BCryptAuth, settings=SETTINGS) app.run() diff --git a/examples/security/hmac.py b/examples/security/hmac.py index fb246c374..976106cb3 100644 --- a/examples/security/hmac.py +++ b/examples/security/hmac.py @@ -55,19 +55,21 @@ class HMACAuth(HMACAuth): - def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, - resource, method): + def check_auth( + self, userid, hmac_hash, headers, data, allowed_roles, resource, method + ): # use Eve's own db driver; no additional connections/resources are used - accounts = app.data.driver.db['accounts'] - user = accounts.find_one({'userid': userid}) + accounts = app.data.driver.db["accounts"] + user = accounts.find_one({"userid": userid}) if user: - secret_key = user['secret_key'] + secret_key = user["secret_key"] # in this implementation we only hash request data, ignoring the # headers. - return user and \ - hmac.new(str(secret_key), str(data), sha1).hexdigest() == hmac_hash + return ( + user and hmac.new(str(secret_key), str(data), sha1).hexdigest() == hmac_hash + ) -if __name__ == '__main__': +if __name__ == "__main__": app = Eve(auth=HMACAuth, settings=SETTINGS) app.run() diff --git a/examples/security/roles.py b/examples/security/roles.py index dd479dc25..85ce2a048 100644 --- a/examples/security/roles.py +++ b/examples/security/roles.py @@ -33,15 +33,15 @@ class RolesAuth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): # use Eve's own db driver; no additional connections/resources are used - accounts = app.data.driver.db['accounts'] - lookup = {'username': username} + accounts = app.data.driver.db["accounts"] + lookup = {"username": username} if allowed_roles: # only retrieve a user if his roles match ``allowed_roles`` - lookup['roles'] = {'$in': allowed_roles} + lookup["roles"] = {"$in": allowed_roles} account = accounts.find_one(lookup) - return account and check_password_hash(account['password'], password) + return account and check_password_hash(account["password"], password) -if __name__ == '__main__': +if __name__ == "__main__": app = Eve(auth=RolesAuth, settings=SETTINGS) app.run() diff --git a/examples/security/settings_security.py b/examples/security/settings_security.py index 5c08a6e58..f064db7c7 100644 --- a/examples/security/settings_security.py +++ b/examples/security/settings_security.py @@ -1,35 +1,17 @@ # -*- coding: utf-8 -*- SETTINGS = { - 'DEBUG': True, - 'MONGO_HOST': 'localhost', - 'MONGO_PORT': 27017, - 'MONGO_DBNAME': 'test_db', - 'DOMAIN': {'accounts': { - 'username': { - 'type': 'string', - 'minlength': 5, - 'maxlength': 20, - }, - 'password': { - 'type': 'string', - 'minlength': 5, - 'maxlength': 20, - }, - 'secret_key': { - 'type': 'string', - 'minlength': 5, - 'maxlength': 20, - }, - 'roles': { - 'type': 'string', - 'minlength': 10, - 'maxlength': 50, - }, - 'token': { - 'type': 'string', - 'minlength': 10, - 'maxlength': 50, - }, - }} + "DEBUG": True, + "MONGO_HOST": "localhost", + "MONGO_PORT": 27017, + "MONGO_DBNAME": "test_db", + "DOMAIN": { + "accounts": { + "username": {"type": "string", "minlength": 5, "maxlength": 20}, + "password": {"type": "string", "minlength": 5, "maxlength": 20}, + "secret_key": {"type": "string", "minlength": 5, "maxlength": 20}, + "roles": {"type": "string", "minlength": 10, "maxlength": 50}, + "token": {"type": "string", "minlength": 10, "maxlength": 50}, + } + }, } diff --git a/examples/security/sha1-hmac.py b/examples/security/sha1-hmac.py index e5852a15f..c3ec77d12 100644 --- a/examples/security/sha1-hmac.py +++ b/examples/security/sha1-hmac.py @@ -31,12 +31,11 @@ class Sha1Auth(BasicAuth): def check_auth(self, username, password, allowed_roles, resource, method): # use Eve's own db driver; no additional connections/resources are used - accounts = app.data.driver.db['accounts'] - account = accounts.find_one({'username': username}) - return account and \ - check_password_hash(account['password'], password) + accounts = app.data.driver.db["accounts"] + account = accounts.find_one({"username": username}) + return account and check_password_hash(account["password"], password) -if __name__ == '__main__': +if __name__ == "__main__": app = Eve(auth=Sha1Auth, settings=SETTINGS) app.run() diff --git a/examples/security/token.py b/examples/security/token.py index 328a443ab..416d71282 100644 --- a/examples/security/token.py +++ b/examples/security/token.py @@ -30,14 +30,14 @@ class TokenAuth(TokenAuth): def check_auth(self, token, allowed_roles, resource, method): """For the purpose of this example the implementation is as simple as possible. A 'real' token should probably contain a hash of the - username/password combo, which should be then validated against the + username/password combo, which should be then validated against the account data stored on the DB. """ # use Eve's own db driver; no additional connections/resources are used - accounts = app.data.driver.db['accounts'] - return accounts.find_one({'token': token}) + accounts = app.data.driver.db["accounts"] + return accounts.find_one({"token": token}) -if __name__ == '__main__': +if __name__ == "__main__": app = Eve(auth=TokenAuth, settings=SETTINGS) app.run() diff --git a/setup.py b/setup.py index 645a43759..d658b400f 100755 --- a/setup.py +++ b/setup.py @@ -1,77 +1,67 @@ #!/usr/bin/env python import io import re -import importlib from setuptools import setup, find_packages -DESCRIPTION = ("Python REST API for Humans.") -with open('README.rst') as f: +DESCRIPTION = "Python REST API for Humans." +with open("README.rst") as f: LONG_DESCRIPTION = f.read() -with io.open('eve/__init__.py', 'rt', encoding='utf8') as f: - VERSION = re.search(r'__version__ = \'(.*?)\'', f.read()).group(1) +with io.open("eve/__init__.py", "rt", encoding="utf8") as f: + VERSION = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1) INSTALL_REQUIRES = [ - 'cerberus>=1.1', - 'events>=0.3,<0.4', - 'flask>=1.0', - 'pymongo>=3.5', - 'simplejson>=3.3.0,<4.0', + "cerberus>=1.1", + "events>=0.3,<0.4", + "flask>=1.0", + "pymongo>=3.5", + "simplejson>=3.3.0,<4.0", ] EXTRAS_REQUIRE = { - "docs": [ - "sphinx", - "alabaster", - "sphinxcontrib-embedly" - ], - "tests": [ - "redis", - "testfixtures", - "pytest", - "tox", - ], + "docs": ["sphinx", "alabaster", "sphinxcontrib-embedly"], + "tests": ["redis", "testfixtures", "pytest", "tox"], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] setup( - name='Eve', + name="Eve", version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, - author='Nicola Iarocci', - author_email='eve@nicolaiarocci.com', - url='http://python-eve.org', + author="Nicola Iarocci", + author_email="eve@nicolaiarocci.com", + url="http://python-eve.org", project_urls={ - 'Documentation': 'http://python-eve.org', - 'Code': 'https://github.com/pyeve/eve', - 'Issue tracker': 'https://github.com/pyeve/eve/issues', + "Documentation": "http://python-eve.org", + "Code": "https://github.com/pyeve/eve", + "Issue tracker": "https://github.com/pyeve/eve/issues", }, - license='BSD', + license="BSD", platforms=["any"], packages=find_packages(), test_suite="eve.tests", install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", ], ) From 4bf26d9a9cd5d7794a47a6d6f79e14c14a9696ef Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 29 May 2018 16:42:10 +0200 Subject: [PATCH 342/821] Support pre-commit and tox linting - If pre-commit is installed linting is performed on every commit. Linting checks and fixes are only applied to staged files. - 'tox -e linting' will perform linting checks and fixes on all files in the repository Closes #1157. --- .pre-commit-config.yaml | 15 +++++++++ CONTRIBUTING.rst | 72 ++++++++++++++++++----------------------- tox.ini | 26 +++++++++------ 3 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..1c826d05a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + args: [--quiet, --safe] + python_version: python3.6 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: flake8 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2577ffb91..2aea40ec8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -39,8 +39,10 @@ Submitting patches - Include tests if your patch is supposed to solve a bug, and explain clearly under which circumstances the bug happens. Make sure the test fails without your patch. -- Follow `PEP8`_. CI will reject a change that does not conform to the - guidelines. +- Enable and install pre-commit_ to ensure styleguides and codechecks are + followed. CI will reject a change that does not conform to the guidelines. + +.. _pre-commit: https://pre-commit.com/ First time setup ~~~~~~~~~~~~~~~~ @@ -73,6 +75,14 @@ First time setup pip install -e ".[dev]" +- Install pre-commit_ and then activate its hooks. pre-commit is a framework for managing and maintaining multi-language pre-commit hooks. Eve uses pre-commit to ensure code-style and code formatting is the same:: + + $ pip install --user pre-commit + $ pre-commit install + + Afterwards, pre-commit will run whenever you commit. + + .. _GitHub account: https://github.com/join .. _latest version of git: https://git-scm.com/downloads .. _username: https://help.github.com/articles/setting-your-username-in-git/ @@ -101,55 +111,35 @@ Start coding Running the tests ~~~~~~~~~~~~~~~~~ -Run the basic test suite with:: - - pytest - -If you want you can run a single module, say the ``methods`` suite:: - - pytest eve/tests/methods/ - -Or, to run only the ``get`` tests:: - - pytest eve/tests/methods/get.py - -You can also choose to just run a single class:: +You should have both Python 2.7 and 3.6 available in your system. Now +running tests is as simple as issuing this command:: - pytest eve/tests/methods/get.py::TestGet + $ tox -e linting,py27,py36 -Or even a single test:: +This command will run tests via the "tox" tool against Python 2.7 and 3.6 and +also perform "lint" coding-style checks. - pytest eve/tests/methods/get.py::TestGet::test_get_emtpy_resource +You can pass different options to ``tox``. For example, to run tests on Python +2.7 and pass options to pytest (e.g. enter pdb on failure) to pytest you can +do:: -You can also collect tests by keyword:: + $ tox -e py27 -- --pdb - pytest -k auth +Or to only run tests in a particular test module on Python 3.6:: -These only runs the tests for the current environment. Whether this is relevant -depends on which part of Eve you're working on. Travis-CI will run the full -suite when you submit your pull request. + $ tox -e py36 -- -k TestGet -The full test suite takes a long time to run because it tests multiple -combinations of Python and dependencies. You need to have Python 2.7, 3.4, -3.5, 3.6, and PyPy installed to run all of the environments. Then run:: +Travis-CI will run the full suite when you submit your pull request. The full +test suite takes a long time to run because it tests multiple combinations of +Python and dependencies. You need to have Python 2.7, 3.4, 3.5, 3.6, and PyPy +installed to run all of the environments. Then run:: tox -Or, if you want to only run your tests against a specific Python environment:: - - tox -e py36 - # py27 = Python 2.7 - # py34 = Python 3.4 - # py35 = Python 3.5 - # py36 = Python 3.6 - # pypy + PyPy - -Rate limiting tests -~~~~~~~~~~~~~~~~~~~ -While there are no test requirements for most of the suite, please be advised -that in order to execute the :ref:`ratelimiting` tests you need a running -Redis_ server. The Rate-Limiting tests are silently skipped if any of the two -conditions are not met. +Please note that you need an active MongoDB instance running on localhost in +order for the tests run. Also, be advived that in order to execute the +:ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting +tests are silently skipped if any of the two conditions are not met. Building the docs ~~~~~~~~~~~~~~~~~ diff --git a/tox.ini b/tox.ini index 08d30d141..7ecf4a452 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,25 @@ [tox] -envlist=py27,py34,py35,py36,pypy +envlist=py27,py34,py35,py36,pypy,linting [testenv] extras=tests commands=py.test eve {posargs} -[testenv:flake8] -deps=flake8 -basepython=python3 -commands=flake8 --ignore=E731,E722,F821 eve {posargs} +[testenv:linting] +skipsdist = True +usedevelop = True +basepython = python3.6 +deps = pre-commit +commands = pre-commit run --all-files [travis] python = - 2.7: py27,flake8 - 3.4: py34,flake8 - 3.5: py35,flake8 - 3.6: py36,flake8 - pypy: pypy,flake8 + 2.7: py27 + 3.4: py34 + 3.5: py35 + 3.6: py36 + pypy: pypy + +[flake8] +max-line-length = 88 +ignore = E401,E722,W503,F821,E501,E203 From 0197426d051f2eec07b1faa82c23c8b8c561bd6c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 29 May 2018 17:35:58 +0200 Subject: [PATCH 343/821] Add linting stage to CI - Performm linting checks as first stage. On failure, do not run the test suite. Closes #1156. --- .travis.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db62b10c5..0a24e5c05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ sudo: false language: python +stages: + - linting + - test cache: pip -script: tox +script: tox --recreate python: - 2.7 - 3.4 @@ -15,3 +18,16 @@ services: before_script: - sleep 15 - mongo eve_test --eval 'db.createUser({user:"test_user",pwd:"test_pw",roles:["readWrite"]});' + +jobs: + include: + - stage: linting + python: '3.6' + env: + install: + - pip install pre-commit + - pre-commit install-hooks + before_script: + services: + script: + - pre-commit run --all-files From f0bd84118f24a23541f37a1959e238c20202c846 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 30 May 2018 09:21:59 +0200 Subject: [PATCH 344/821] Changelog update for #1155, #1156, #1157 --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3ab5a6c33..bab2b1987 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,11 @@ Fixed Improved ~~~~~~~~ +- Perform lint checks and fixes on staged files, as a pre-commit hook. + (`#1157`_) +- On CI, perform linting checks first. If linting checks are successful, + execute the test suite on the whole matrix. (`#1156`_) +- Reformat code to match Black code-style. (`#1155`_) - Fix broken link to the Postman app. (`#1150`_) - Update obsolete PyPI link in docs sidebar. (`#1152`_) - Only display the version number on the docs homepage. (`#1151`_) @@ -39,6 +44,9 @@ Improved .. _`#1150`: https://github.com/pyeve/eve/issues/1150 .. _`#1112`: https://github.com/pyeve/eve/issues/1112 .. _`#1154`: https://github.com/pyeve/eve/issues/1154 +.. _`#1155`: https://github.com/pyeve/eve/issues/1155 +.. _`#1156`: https://github.com/pyeve/eve/issues/1156 +.. _`#1157`: https://github.com/pyeve/eve/issues/1157 Version 0.8 ----------- From 4de2acf150d9cc4e582f9a101ddce80d86530f7a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 31 May 2018 11:39:48 +0200 Subject: [PATCH 345/821] Fix speakerdeck embedding Closes #1158. --- CHANGES.rst | 11 ++++++++--- docs/rest_api_for_humans.rst | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bab2b1987..26e63dc4d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,15 +22,19 @@ Improved - On CI, perform linting checks first. If linting checks are successful, execute the test suite on the whole matrix. (`#1156`_) - Reformat code to match Black code-style. (`#1155`_) +- Use ``simplejson`` everywhere in the codebase. (`#1148`_) +- Install a bot that flags and closes stale issues/pull requests. (`#1145`_) +- Only set the package version in ``__init__.py``. (`#1142`_) + +Docs +~~~~ +- Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) - Update obsolete PyPI link in docs sidebar. (`#1152`_) - Only display the version number on the docs homepage. (`#1151`_) -- Use ``simplejson`` everywhere in the codebase. (`#1148`_) - Fix documentation builds on Read the Docs. (`#1147`_) - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) -- Install a bot that flags and closes stale issues/pull requests. (`#1145`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) -- Only set the package version in ``__init__.py``. (`#1142`_) .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 @@ -47,6 +51,7 @@ Improved .. _`#1155`: https://github.com/pyeve/eve/issues/1155 .. _`#1156`: https://github.com/pyeve/eve/issues/1156 .. _`#1157`: https://github.com/pyeve/eve/issues/1157 +.. _`#1158`: https://github.com/pyeve/eve/issues/1158 Version 0.8 ----------- diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index 7388dc6fa..b687c3433 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -6,7 +6,7 @@ rundown on Eve features, along with a few code snippets and examples. Hopefully it will do a good job in letting you decide whether Eve is valid solution for your use case. -.. embedly:: http://speakerdeck.com/nicola/eve-rest-api-for-humans +.. embedly:: https://speakerdeck.com/nicola/eve-rest-api-for-humans Conferences ------------ From eb6d37dccf46af5e173da5601bdce0ee961e401b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 1 Jun 2018 09:21:11 +0200 Subject: [PATCH 346/821] Fix: write failure on nullable data relation fields Closes #1159. --- CHANGES.rst | 2 ++ eve/io/mongo/validation.py | 3 +++ eve/tests/methods/post.py | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 26e63dc4d..85516d368 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- Updating a field with a nullable data relation fails when value is null (`#1159`_) - ``cerberus.schema.SchemaError`` when ``VALIDATE_FILTERS = True``. (`#1154`_) - Serializers fails when array of types is in schema. (`#1112`_) - Replace the broken ``make audit`` shortcut with ``make check``, add the @@ -52,6 +53,7 @@ Docs .. _`#1156`: https://github.com/pyeve/eve/issues/1156 .. _`#1157`: https://github.com/pyeve/eve/issues/1157 .. _`#1158`: https://github.com/pyeve/eve/issues/1158 +.. _`#1159`: https://github.com/pyeve/eve/issues/1159 Version 0.8 ----------- diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 6f798b8ec..417ff4646 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -128,6 +128,9 @@ def _validate_data_relation(self, data_relation, field, value): 'embeddable': {'type': 'boolean', 'default': False}, 'version': {'type': 'boolean', 'default': False} }} """ + if not value and self.schema[field].get("nullable"): + return + if "version" in data_relation and data_relation["version"] is True: value_field = data_relation["field"] version_field = app.config["VERSION"] diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index da14beacb..201906dd7 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -888,6 +888,31 @@ def test_post_custom_json_content_type(self): ) self.assert201(status) + def test_post_updating_a_document_with_nullable_data_relation_does_not_fail(self): + # See #1159. + del (self.domain["contacts"]["schema"]["ref"]["required"]) + + employee = { + "employer": { + "type": "objectid", + "nullable": True, + "data_relation": {"resource": self.known_resource}, + } + } + self.app.register_resource("employee", {"schema": employee}) + + data = {"employer": None} + r, s = self.post("employee", data=data) + self.assert201(s) + + employee["employer"]["nullable"] = False + r, s = self.post("employee", data=data) + self.assert422(s) + + del (employee["employer"]["nullable"]) + r, s = self.post("employee", data=data) + self.assert422(s) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From 0360bf90340fc3ae5f0230913e6f171add04caa6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 11 Jun 2018 17:52:45 +0200 Subject: [PATCH 347/821] Fix: allow_unknown failure on mapping fields Closes #1163 --- CHANGES.rst | 2 ++ eve/methods/patch.py | 4 +++- eve/methods/post.py | 9 ++++++++- eve/methods/put.py | 4 +++- eve/tests/methods/post.py | 15 +++++++++++++++ eve/validation.py | 4 ---- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 85516d368..67b001915 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) - Updating a field with a nullable data relation fails when value is null (`#1159`_) - ``cerberus.schema.SchemaError`` when ``VALIDATE_FILTERS = True``. (`#1154`_) - Serializers fails when array of types is in schema. (`#1112`_) @@ -54,6 +55,7 @@ Docs .. _`#1157`: https://github.com/pyeve/eve/issues/1157 .. _`#1158`: https://github.com/pyeve/eve/issues/1158 .. _`#1159`: https://github.com/pyeve/eve/issues/1159 +.. _`#1163`: https://github.com/pyeve/eve/issues/1163 Version 0.8 ----------- diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 5e5acf6e1..afe48e6be 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -152,7 +152,9 @@ def patch_internal( resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] - validator = app.validator(schema, resource=resource) + validator = app.validator( + schema, resource=resource, allow_unknown=resource_def["allow_unknown"] + ) object_id = original[resource_def["id_field"]] last_modified = None diff --git a/eve/methods/post.py b/eve/methods/post.py index cfcb20f5e..23d20ba0d 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -160,7 +160,14 @@ def post_internal(resource, payl=None, skip_validation=False): date_utc = datetime.utcnow().replace(microsecond=0) resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] - validator = None if skip_validation else app.validator(schema, resource=resource) + validator = ( + None + if skip_validation + else app.validator( + schema, resource=resource, allow_unknown=resource_def["allow_unknown"] + ) + ) + documents = [] results = [] failures = 0 diff --git a/eve/methods/put.py b/eve/methods/put.py index 86cba5ffd..603551504 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -124,7 +124,9 @@ def put_internal( """ resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] - validator = app.validator(schema, resource=resource) + validator = app.validator( + schema, resource=resource, allow_unknown=resource_def["allow_unknown"] + ) if payload is None: payload = payload_() diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 201906dd7..e94417afc 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -420,6 +420,21 @@ def test_post_allow_unknown(self): self.assertTrue("unknown" in r_data) self.assertEqual("unknown", r_data["unknown"]) + def test_post_mapping_allow_unknown_allowed(self): + schema = { + "data": { + "type": "dict", + "allow_unknown": True, + "schema": {"prop": {"type": "string"}}, + } + } + settings = {"RESOURCE_METHODS": ["GET", "POST", "DELETE"], "schema": schema} + self.app.register_resource("endpoint", settings) + + data = {"data": {"prop": "test prop", "test": "test"}} + r, status = self.post("endpoint", data=data) + self.assert201(status) + def test_post_with_content_type_charset(self): test_field = "ref" test_value = "1234567890123456789054321" diff --git a/eve/validation.py b/eve/validation.py index 48b7fc3e1..7ba88b155 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -25,10 +25,6 @@ def __init__(self, *args, **kwargs): if not config.VALIDATION_ERROR_AS_LIST: kwargs["error_handler"] = SingleErrorAsStringErrorHandler - resource = kwargs.get("resource", None) - if resource: - resource_def = config.DOMAIN[resource] - kwargs["allow_unknown"] = resource_def["allow_unknown"] super(Validator, self).__init__(*args, **kwargs) def validate_update(self, document, document_id, persisted_document=None): From 3937c126f33fa5a426af985cf4d33550a4544108 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 18 Jun 2018 14:55:07 +0200 Subject: [PATCH 348/821] Fix TypeError with doc embedding and soft deletes Closes #1120 --- CHANGES.rst | 2 ++ eve/methods/common.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 67b001915..fc8d37b0f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- ``TypeError argument of type 'NoneType' is not iterable`` error when using document embedding in conjuction with soft deletes (`#1120`_) - ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) - Updating a field with a nullable data relation fails when value is null (`#1159`_) - ``cerberus.schema.SchemaError`` when ``VALIDATE_FILTERS = True``. (`#1154`_) @@ -56,6 +57,7 @@ Docs .. _`#1158`: https://github.com/pyeve/eve/issues/1158 .. _`#1159`: https://github.com/pyeve/eve/issues/1159 .. _`#1163`: https://github.com/pyeve/eve/issues/1163 +.. _`#1120`: https://github.com/pyeve/eve/issues/1120 Version 0.8 ----------- diff --git a/eve/methods/common.py b/eve/methods/common.py index ac8841a01..34ca7d00a 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -985,7 +985,7 @@ def resolve_embedded_documents(document, resource, embedded_fields): fields_chain = field.split(".") last_field = fields_chain[-1] for subdocument in subdocuments(fields_chain[:-1], resource, document): - if last_field not in subdocument: + if not subdocument or last_field not in subdocument: continue subdocument[last_field] = getter(subdocument[last_field]) From 87eac6d2c39b2ea694ed3e57b33ab3e6ccc6e735 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 9 Jul 2018 16:31:15 +0200 Subject: [PATCH 349/821] Add missing Eve talks to docs --- docs/rest_api_for_humans.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index b687c3433..5f858be12 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -12,6 +12,8 @@ Conferences ------------ Eve REST API for Humans™ has been presented at the following events so far: +- PyConWeb 2018, Munich +- PyCon Belarus 2018, Kiev - Codemotion 2017, Rome - PiterPy 2016, St. Petersburg - Percona Live 2015, Amsterdam From 7e2f53d14f36a0b41742eb7d97108479c012a7aa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jul 2018 14:21:22 +0200 Subject: [PATCH 350/821] Pin Flask-PyMongo dependency to avoid crash with v2 Closes #1172 --- CHANGES.rst | 5 +++++ setup.py | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fc8d37b0f..335d8d7b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -241,6 +241,11 @@ Breaking Changes .. _`How to contribute`: http://python-eve.org/contributing.html .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course +Version 0.7.10 +~~~~~~~~~~~~~~ + +- Fix: Pin Flask-PyMongo dependency to avoid crash with Flask-PyMongo 2. Closes #1172. + Version 0.7.9 ~~~~~~~~~~~~~ diff --git a/setup.py b/setup.py index d658b400f..6e5d85341 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ with io.open("eve/__init__.py", "rt", encoding="utf8") as f: VERSION = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1) - INSTALL_REQUIRES = [ "cerberus>=1.1", "events>=0.3,<0.4", From 11f7c08de0d37204e6aefa30c7e227b575309cf4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 15 Jul 2018 14:24:15 +0200 Subject: [PATCH 351/821] Bump version to 0.7.10 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 335d8d7b4..134df980d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -244,6 +244,8 @@ Breaking Changes Version 0.7.10 ~~~~~~~~~~~~~~ +Released on July 15, 2018. + - Fix: Pin Flask-PyMongo dependency to avoid crash with Flask-PyMongo 2. Closes #1172. Version 0.7.9 From b6b79231da765ebeefb30b1b952911f953b1fb1f Mon Sep 17 00:00:00 2001 From: quentinpraz Date: Wed, 25 Jul 2018 21:17:26 +0200 Subject: [PATCH 352/821] Fixing the copy() issue when using Python 2.7.X --- eve/flaskapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index f4dc33d05..6cf7741b7 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -294,7 +294,7 @@ def deprecated_renderers_settings(): msg = "{} setting is deprecated and will be removed" " in future release. Please use RENDERERS instead." if "JSON" in self.config or "XML" in self.config: - self.config["RENDERERS"] = default_settings.RENDERERS.copy() + self.config["RENDERERS"] = default_settings.RENDERERS[:] if "JSON" in self.config: warnings.warn(msg.format("JSON")) From 0ad2f100671509988c781137c83982b1168b9bf8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 31 Jul 2018 10:16:27 +0200 Subject: [PATCH 353/821] Add regression test for PR #1177 --- eve/tests/renders.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 5b1846d37..423295645 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -339,3 +339,11 @@ def test_CORS_OPTIONS_schema(self): self.app._init_schema_endpoint() methods = ["GET", "OPTIONS"] self.test_CORS_OPTIONS("schema", methods) + + def test_deprecated_renderers_supports_py27(self): + """ Make sure #1175 is fixed """ + self.app.config["JSON"] = False + try: + self.app.check_deprecated_features() + except AttributeError: + self.fail("AttributeError raised unexpectedly.") From 8869b78eb45192df34ad2b271ef3b487de07b1a6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 31 Jul 2018 10:20:56 +0200 Subject: [PATCH 354/821] Changelog update for #1177 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 134df980d..3977592b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- ``AttributeError`` on Python 2.7 when obsolete ``JSON`` or ``XML`` settings are used (`#1175`_). - ``TypeError argument of type 'NoneType' is not iterable`` error when using document embedding in conjuction with soft deletes (`#1120`_) - ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) - Updating a field with a nullable data relation fails when value is null (`#1159`_) @@ -39,6 +40,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`#1175`: https://github.com/pyeve/eve/issues/1175 .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 .. _`#1144`: https://github.com/pyeve/eve/issues/1144 From 48bf8e52078249fdaac7ad34965d27884a2a25fa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 31 Jul 2018 10:22:45 +0200 Subject: [PATCH 355/821] quentinpraz --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ab7f12e52..d522fb0dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -171,4 +171,5 @@ Patches and Contributions - dccrazyboy - kreynen - mmizotin +- quentinpraz - xgdgsc From a6ef194be907c207cfa480ed33c570bd115b9719 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 3 Aug 2018 15:17:01 +0200 Subject: [PATCH 356/821] Fix: OperationFailure on Mongo full text searches Closes #1176 --- CHANGES.rst | 2 ++ eve/io/base.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3977592b3..6a67387d8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased Fixed ~~~~~ +- v0.8: ``OperationFailure`` performing MongoDB full text searches (`#1176`_) - ``AttributeError`` on Python 2.7 when obsolete ``JSON`` or ``XML`` settings are used (`#1175`_). - ``TypeError argument of type 'NoneType' is not iterable`` error when using document embedding in conjuction with soft deletes (`#1120`_) - ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) @@ -40,6 +41,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`#1176`: https://github.com/pyeve/eve/issues/1176 .. _`#1175`: https://github.com/pyeve/eve/issues/1175 .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 diff --git a/eve/io/base.py b/eve/io/base.py index 3b01adade..650b5fc65 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -464,7 +464,7 @@ def _datasource_ex( fields = client_projection # always drop exclusion projection, thus avoid mixed projection not # supported by db driver - fields = dict([(field, 1) for field, value in fields.items() if value]) + fields = dict([(field, value) for field, value in fields.items() if value]) # If the current HTTP method is in `public_methods` or # `public_item_methods`, skip the `auth_field` check From 0eb6f93a0e55d3d7780ca5bc68389ed9989f719f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 27 Aug 2018 09:16:10 +0200 Subject: [PATCH 357/821] NORMALIZE_DOTTED_FIELDS/normalize_dotted_fields If True, dotted fields are parsed and processed as subdocument fields. If False, dotted fields are left unparsed and unprocessed and the payload is passed to the underlying data-layer as-is. Please note that with the default Mongo layer, setting this to False will result in an error. Defaults to True. Closes #1173 --- CHANGES.rst | 11 +++++++++++ docs/config.rst | 17 +++++++++++++++++ eve/default_settings.py | 1 + eve/flaskapp.py | 3 +++ eve/methods/common.py | 9 ++++++++- eve/tests/config.py | 1 + eve/tests/methods/post.py | 16 ++++++++++++++++ 7 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6a67387d8..6369a0118 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,16 @@ Version 0.8.1 Unreleased +New +~~~ +- ``NORMALIZE_DOTTED_FIELDS``. If ``True``, dotted fields are parsed and + processed as subdocument fields. If ``False``, dotted fields are left + unparsed and unprocessed and the payload is passed to the underlying + data-layer as-is. Please note that with the default Mongo layer, setting this + to ``False`` will result in an error. Defaults to ``True``. (`#1173`_) +- ``normalize_dotted_fields``. Endpoint-level override + for ``NORMALIZE_DOTTED_FIELDS``. (`#1173`_) + Fixed ~~~~~ - v0.8: ``OperationFailure`` performing MongoDB full text searches (`#1176`_) @@ -43,6 +53,7 @@ Docs .. _`#1176`: https://github.com/pyeve/eve/issues/1176 .. _`#1175`: https://github.com/pyeve/eve/issues/1175 +.. _`#1173`: https://github.com/pyeve/eve/issues/1173 .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 .. _`#1144`: https://github.com/pyeve/eve/issues/1144 diff --git a/docs/config.rst b/docs/config.rst index 8be9dfa43..7aca19b49 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -768,6 +768,15 @@ uppercase. If ``False``, the updates overwrite the current data. Defaults to ``True``. +``NORMALIZE_DOTTED_FIELDS`` If ``True``, dotted fields are parsed + and processed as subdocument fields. If + ``False``, dotted fields are left unparsed + and unprocessed, and the payload is passed + to the underlying data-layer as-is. Please + note that with the default Mongo layer, + setting this to ``False`` will result in an + error. Defaults to ``True``. + =================================== ========================================= .. _domain: @@ -1103,6 +1112,14 @@ always lowercase. If ``False``, the updates overwrite the current data. Locally overrides ``MERGE_NESTED_DOCUMENTS``. +``normalize_dotted_fields`` If ``True``, dotted fields are parsed and + processed as subdocument fields. If ``False``, + dotted fields are left unparsed and + unprocessed, and the payload is passed to the + underlying data-layer as-is. Please note that + with the default Mongo layer, setting this to + ``False`` will result in an error. Defaults to + ``True``. =============================== =============================================== diff --git a/eve/default_settings.py b/eve/default_settings.py index 7f318618f..7d092fb3c 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -120,6 +120,7 @@ META = "_meta" INFO = None VALIDATION_ERROR_STATUS = 422 +NORMALIZE_DOTTED_FIELDS = True # return a single field validation error as a list (by default a single error # is retuned as string, while multiple errors are returned as a list). diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 6cf7741b7..42f76d2a1 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -675,6 +675,9 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault( "merge_nested_documents", self.config["MERGE_NESTED_DOCUMENTS"] ) + settings.setdefault( + "normalize_dotted_fields", self.config["NORMALIZE_DOTTED_FIELDS"] + ) # empty schemas are allowed for read-only access to resources schema = settings.setdefault("schema", {}) self.set_schema_defaults(schema, settings["id_field"]) diff --git a/eve/methods/common.py b/eve/methods/common.py index 34ca7d00a..6c0980458 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -377,6 +377,9 @@ def serialize(document, resource=None, schema=None, fields=None): """ Recursively handles field values that require data-aware serialization. Relies on the app.data.serializers dictionary. + .. versionchanged: 0.8.1 + Normalize dotted fields according to normalized_dotted_fields. See #1173. + .. versionchanged:: 0.7 Add support for normalizing anyof-like rules inside lists. See #876. @@ -402,7 +405,11 @@ def serialize(document, resource=None, schema=None, fields=None): def resolve_schema(schema): return schema if isinstance(schema, dict) else schema_registry.get(schema) - normalize_dotted_fields(document) + if ( + resource not in config.DOMAIN + or config.DOMAIN[resource]["normalize_dotted_fields"] + ): + normalize_dotted_fields(document) if app.data.serializers: if resource: diff --git a/eve/tests/config.py b/eve/tests/config.py index d0883135c..a002b7e15 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -90,6 +90,7 @@ def test_default_settings(self): self.assertEqual( self.app.config["JSON_REQUEST_CONTENT_TYPES"], ["application/json"] ) + self.assertEqual(self.app.config["NORMALIZE_DOTTED_FIELDS"], True) def test_settings_as_dict(self): my_settings = {"API_VERSION": "override!", "DOMAIN": {"contacts": {}}} diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index e94417afc..97e30e269 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -928,6 +928,22 @@ def test_post_updating_a_document_with_nullable_data_relation_does_not_fail(self r, s = self.post("employee", data=data) self.assert422(s) + def test_post_dont_normalize_dotted_fields(self): + # Allow skipping of default dotted field normalization, mostly useful + # for custom data layers such as eve_elastic. See #1173. + self.app.register_resource( + "test", + {"normalize_dotted_fields": False, "schema": {"a_dict": {"type": "dict"}}}, + ) + + data = {"a_dict": {"dotted.field": True}} + headers = [("Content-Type", "application/json")] + resp = self.test_client.post("test/", data=json.dumps(data), headers=headers) + _, status = self.parse_response(resp) + # mongo returns bson.errors.InvalidDocument: + # key 'dotted.fields' must not contain '.' + self.assertEqual(500, status) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From e71dc23feba5dc6ad97cfc7b45d6ecab4c38e9ff Mon Sep 17 00:00:00 2001 From: Wan Bachtiar Date: Tue, 28 Aug 2018 12:46:54 +1000 Subject: [PATCH 358/821] Added support $centerSphere operator --- eve/io/mongo/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 4ed3dec39..69a3f2c67 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -113,7 +113,7 @@ class Mongo(DataLayer): + ["$mod", "$regex", "$text", "$where"] + ["$options", "$search", "$language", "$caseSensitive"] + ["$diacriticSensitive", "$exists", "$type"] - + ["$geoWithin", "$geoIntersects", "$near", "$nearSphere"] + + ["$geoWithin", "$geoIntersects", "$near", "$nearSphere", "$centerSphere"] + ["$geometry", "$maxDistance", "$box"] + ["$all", "$elemMatch", "$size"] + ["$bitsAllClear", "$bitsAllSet", "$bitsAnyClear", "$bitsAnySet"] From 2d8056ed9c4c90b7d21d412032c8b26cd21a6390 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 28 Aug 2018 09:24:31 +0200 Subject: [PATCH 359/821] Changelog for #1181 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6369a0118..028d41490 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased New ~~~ +- Add support for Mongo ``$centerSphere`` query operator (`#1181`_) - ``NORMALIZE_DOTTED_FIELDS``. If ``True``, dotted fields are parsed and processed as subdocument fields. If ``False``, dotted fields are left unparsed and unprocessed and the payload is passed to the underlying @@ -51,6 +52,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`#1181`: https://github.com/pyeve/eve/issues/1181 .. _`#1176`: https://github.com/pyeve/eve/issues/1176 .. _`#1175`: https://github.com/pyeve/eve/issues/1175 .. _`#1173`: https://github.com/pyeve/eve/issues/1173 From 388f1dd50c92c6cf9c32ea9d0539e61a02d7054b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 28 Aug 2018 09:26:06 +0200 Subject: [PATCH 360/821] Wan Bachtiar --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d522fb0dc..83db63654 100644 --- a/AUTHORS +++ b/AUTHORS @@ -165,6 +165,7 @@ Patches and Contributions - Valerie Coffman - Vasilis Lolis - Wael M. Nasreddine +- Wan Bachtiar - Wei Guan - Xavi Cubillas - boosh From 5f5d5237f2d61dec3394304cf53978e0c6f8e076 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 29 Aug 2018 09:59:56 +0200 Subject: [PATCH 361/821] Add MONGO_AUTH_SOURCE to Quickstart snippet Closes #1168. --- CHANGES.rst | 2 ++ docs/quickstart.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 028d41490..21471954d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -44,6 +44,7 @@ Improved Docs ~~~~ +- Add ``MONGO_AUTH_SOURCE`` to Quickstart. (`#1168`_) - Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) - Update obsolete PyPI link in docs sidebar. (`#1152`_) @@ -56,6 +57,7 @@ Docs .. _`#1176`: https://github.com/pyeve/eve/issues/1176 .. _`#1175`: https://github.com/pyeve/eve/issues/1175 .. _`#1173`: https://github.com/pyeve/eve/issues/1173 +.. _`#1168`: https://github.com/pyeve/eve/issues/1168 .. _`#1142`: https://github.com/pyeve/eve/issues/1142 .. _`#1143`: https://github.com/pyeve/eve/issues/1143 .. _`#1144`: https://github.com/pyeve/eve/issues/1144 diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 191c24872..8409d1610 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -131,6 +131,7 @@ Let's connect to a database by adding the following lines to settings.py: # Skip these if your db has no auth. But it really should. MONGO_USERNAME = '' MONGO_PASSWORD = '' + MONGO_AUTH_SOURCE = 'admin' # needed if --auth mode is enabled MONGO_DBNAME = 'apitest' From c7157ed8afa07ea436d4654d37f5c663f593c4a5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 27 Aug 2018 15:24:41 +0200 Subject: [PATCH 362/821] Fix: OperationFailure when changing keys on existing index Closes #1180 --- CHANGES.rst | 10 ++++++---- eve/io/mongo/mongo.py | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 21471954d..d63ab6dd6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,12 +21,13 @@ New Fixed ~~~~~ -- v0.8: ``OperationFailure`` performing MongoDB full text searches (`#1176`_) -- ``AttributeError`` on Python 2.7 when obsolete ``JSON`` or ``XML`` settings are used (`#1175`_). -- ``TypeError argument of type 'NoneType' is not iterable`` error when using document embedding in conjuction with soft deletes (`#1120`_) +- ``mongo_indexes``: "OperationFailure" when changing the keys of an existing index (`#1180`_) +- v0.8: "OperationFailure" performing MongoDB full text searches (`#1176`_) +- "AttributeError" on Python 2.7 when obsolete ``JSON`` or ``XML`` settings are used (`#1175`_). +- "TypeError argument of type 'NoneType' is not iterable" error when using document embedding in conjuction with soft deletes (`#1120`_) - ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) - Updating a field with a nullable data relation fails when value is null (`#1159`_) -- ``cerberus.schema.SchemaError`` when ``VALIDATE_FILTERS = True``. (`#1154`_) +- "cerberus.schema.SchemaError" when ``VALIDATE_FILTERS = True``. (`#1154`_) - Serializers fails when array of types is in schema. (`#1112`_) - Replace the broken ``make audit`` shortcut with ``make check``, add the command to ``CONTRIBUTING.rst`` it was missing. (`#1144`_) @@ -54,6 +55,7 @@ Docs - Improve changelog format to reduce noise and increase readability. (`#1143`_) .. _`#1181`: https://github.com/pyeve/eve/issues/1181 +.. _`#1180`: https://github.com/pyeve/eve/issues/1180 .. _`#1176`: https://github.com/pyeve/eve/issues/1176 .. _`#1175`: https://github.com/pyeve/eve/issues/1175 .. _`#1173`: https://github.com/pyeve/eve/issues/1173 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 69a3f2c67..99ea7318e 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -1027,6 +1027,9 @@ def _create_index(app, resource, name, list_of_keys, index_options): For example: {"sparse": True} + .. versionchanged:: 0.8.1 + Add support for IndexKeySpecsConflict error. See #1180. + .. versionadded:: 0.6 """ @@ -1056,11 +1059,9 @@ def _create_index(app, resource, name, list_of_keys, index_options): try: coll.create_index(list_of_keys, **kw) except pymongo.errors.OperationFailure as e: - if e.code == 85: - # This error is raised when the definition of the index has - # been changed, we didn't find any spec out there but we - # think that this error is not going to change and we can - # trust. + if e.code in (85, 86): + # raised when the definition of the index has been changed. + # (https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.err#L87) # by default, drop the old index with old configuration and # create the index again with the new configuration. From 618f84b12b286c4a55d942ab2e681e09b2e0cd90 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 10:36:31 +0300 Subject: [PATCH 363/821] typo s/runnig/running/ --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8409d1610..f4b0bf5af 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -98,7 +98,7 @@ This time we also got an ``_items`` list. The ``_links`` are relative to the resource being accessed, so you get a link to the parent resource (the home page) and to the resource itself. If you got a timeout error from pymongo, make sure the prerequistes are met. Chances are that the ``mongod`` server process -is not runnig. +is not running. By default Eve APIs are read-only: From 3e80fef491fc714c2e36de3ae7b2468cd31aa337 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 09:32:17 +0200 Subject: [PATCH 364/821] Chen Rotem --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 83db63654..0c579ea8b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -25,6 +25,7 @@ Patches and Contributions - Bryan Cattle - Carl George - Carles Bruguera +- Chen Rotem - Christian Henke - Christoph Witzany - Christopher Larsen From 6434003840ed908fa9cc2becbf743d9493715894 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 09:33:55 +0200 Subject: [PATCH 365/821] Changelog for #1183 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d63ab6dd6..1200c1182 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,6 +45,7 @@ Improved Docs ~~~~ +- Typos (`#1183`_) - Add ``MONGO_AUTH_SOURCE`` to Quickstart. (`#1168`_) - Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) @@ -54,6 +55,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`1183`: https://github.com/pyeve/eve/pull/1183 .. _`#1181`: https://github.com/pyeve/eve/issues/1181 .. _`#1180`: https://github.com/pyeve/eve/issues/1180 .. _`#1176`: https://github.com/pyeve/eve/issues/1176 From 4da56f46f843435e69cef30014d16fe028b2c042 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 09:35:24 +0200 Subject: [PATCH 366/821] Changelog for #1183 --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1200c1182..c6bf54b3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -55,7 +55,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) -.. _`1183`: https://github.com/pyeve/eve/pull/1183 +.. _`#1183`: https://github.com/pyeve/eve/pull/1183 .. _`#1181`: https://github.com/pyeve/eve/issues/1181 .. _`#1180`: https://github.com/pyeve/eve/issues/1180 .. _`#1176`: https://github.com/pyeve/eve/issues/1176 From 2ce6d4e429c73bd404a1a48a5f63c0fdf9332ffe Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 11:10:43 +0300 Subject: [PATCH 367/821] typo s/concurrenncy/concurrency/ --- docs/features.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index c2d3ac310..0ef9e39b1 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -491,7 +491,7 @@ performance. .. _rendering: Rendering ----------------------- +--------- Eve responses are automatically rendered as JSON (the default) or XML, depending on the request ``Accept`` header. Inbound documents (for inserts and edits) are in JSON format. @@ -608,7 +608,7 @@ control is disabled no ETag is provided with responses. You should be careful about disabling this feature, as you would effectively open your API to the risk of older versions replacing your documents. Alternatively, ETag match checks can be made optional by the client if ``ENFORCE_IF_MATCH`` is disabled. -When concurrenncy check enforcement is disabled, requests with the ``If-Match`` +When concurrency check enforcement is disabled, requests with the ``If-Match`` header will be processed as conditional requests, and requests made without the ``If-Match`` header will not be processed as conditional. From 8d47f5ae0a3eb3c73cd2542c5ab38c266c6430eb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 09:37:46 +0200 Subject: [PATCH 368/821] Changelog for #1184 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c6bf54b3e..d876927a4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,7 +45,7 @@ Improved Docs ~~~~ -- Typos (`#1183`_) +- Typos (`#1183`_, `#1184`_) - Add ``MONGO_AUTH_SOURCE`` to Quickstart. (`#1168`_) - Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) @@ -55,6 +55,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`#1184`: https://github.com/pyeve/eve/pull/1184 .. _`#1183`: https://github.com/pyeve/eve/pull/1183 .. _`#1181`: https://github.com/pyeve/eve/issues/1181 .. _`#1180`: https://github.com/pyeve/eve/issues/1180 From f9ef8499429fb109725f764f8605381c9af29979 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 12:32:59 +0300 Subject: [PATCH 369/821] Typos and minor changes --- docs/features.rst | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 0ef9e39b1..8bea213ff 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -871,7 +871,7 @@ Default and Nullable Values --------------------------- Fields can have default values and nullable types. When serving POST (create) requests, missing fields will be assigned the configured default values. See -``default`` and ``nullable`` keywords in :ref:`schema` for more informations. +``default`` and ``nullable`` keywords in :ref:`schema` for more information. Predefined Database Filters --------------------------- @@ -1376,7 +1376,7 @@ the items as needed before they are returned to the client. It is important to note that fetch events will work with `Document Versioning`_ for specific document versions or accessing all document -versions with ``?version=all``, but they *will not* work when acessing diffs +versions with ``?version=all``, but they *will not* work when accessing diffs of all versions with ``?version=diffs``. @@ -1391,7 +1391,7 @@ These are the insert events with their method signature: - ``on_inserted_(items)`` When a POST requests hits the API and new items are about to be stored in -the database, these vents are fired: +the database, these events are fired: - ``on_insert`` for every resource endpoint. - ``on_insert_`` for the specific `` resource @@ -1450,7 +1450,7 @@ accessory action. After the item has been replaced, these other two events are fired: - ``on_replaced`` for any resource item endpoint. -- ``on_replaced_`` for the specific resource endpont. +- ``on_replaced_`` for the specific resource endpoint. Update Events ^^^^^^^^^^^^^ @@ -1531,8 +1531,10 @@ notified of such a disastrous occurrence by hooking a callback function to the - ``on_delete_resource_originals`` for any resource hit by the request after having retrieved the originals documents. - ``on_delete_resource_originals_`` for the specific `` resource endpoint - hit by the DELETE after having retrieved the original document. NOTE: those two event are useful in order to - perform some business logic before the actual remove operation given the look up and the list of originals + hit by the DELETE after having retrieved the original document. + +NOTE: those two event are useful in order to perform some business logic before the actual remove operation given the +look up and the list of originals .. _aggregation_hooks: @@ -1805,7 +1807,7 @@ additional fields except the file fields will be treated as ``strings`` for all field validation purposes. If you have already defined some of the resource fields to be of different type (boolean, number, list etc) the validation rules for these fields would fail, preventing you to -succesffully submit your resource. +successffully submit your resource. If you still want to be able to perform field validation in this case, you will have to turn on ``MULTIPART_FORM_FIELDS_AS_JSON`` in your settings @@ -1852,7 +1854,7 @@ All these objects are implemented as native Eve data types (see :ref:`schema`) so they are are subject to the proper validation. In the example below we are extending the `people` endpoint by adding -a ``location`` field is of type Point_. +a ``location`` field of type Point_. .. code-block:: javascript @@ -2005,7 +2007,7 @@ time a custom function is invoked. 'url: %(url)s, method:%(method)s')) # the default log level is set to WARNING, so - # we have to explictly set the logging level + # we have to explicitly set the logging level # to INFO to get our custom message logged. app.logger.setLevel(logging.INFO) @@ -2030,7 +2032,7 @@ oplog is simply a server log. What makes it a little bit different is that it can be exposed as a read-only endpoint, thus allowing clients to query it as they would with any other API endpoint. -Every oplog entry contains informations about the document and the operation: +Every oplog entry contains information about the document and the operation: - Operation performed - Unique ID of the document @@ -2086,12 +2088,12 @@ more on this later). Please note that by default the ``c`` (changes) field is not included for ``POST`` operations. You can add ``POST`` to the ``OPLOG_CHANGE_METHODS`` -setting (see :ref:`global`) if you whish the whole document to be included on +setting (see :ref:`global`) if you wish the whole document to be included on every insertion. How is the oplog operated? ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Six settings are dedicated to the OpLog: +Seven settings are dedicated to the OpLog: - ``OPLOG`` switches the oplog feature on and off. Defaults to ``False``. - ``OPLOG_NAME`` is the name of the oplog collection on the database. Defaults to ``oplog``. @@ -2104,7 +2106,7 @@ Six settings are dedicated to the OpLog: As you can see the oplog feature is turned off by default. Also, since ``OPLOG_ENDPOINT`` defaults to ``None``, even if you switch the feature on no -public oplog endpoint will be available. You will have to explictly set the +public oplog endpoint will be available. You will have to explicitly set the endpoint name in order to expose your oplog to the public. The Oplog endpoint @@ -2152,13 +2154,13 @@ each entry: app.on_oplog_push += oplog_extras app.run() -Please note that unless you explictly set ``OPLOG_RETURN_EXTRA_FIELD`` to +Please note that unless you explicitly set ``OPLOG_RETURN_EXTRA_FIELD`` to ``True``, the ``extra`` field will *not* be returned by the ``OPLOG_ENDPOINT``. .. note:: Are you on MongoDB? Consider making the oplog a `capped collection`_. Also, - in case you are wondering yes, the Eve oplog is blatantly inpsired by the + in case you are wondering yes, the Eve oplog is blatantly inspired by the awesome `Replica Set Oplog`_. .. _schema_endpoint: @@ -2169,7 +2171,7 @@ Resource schema can be exposed to API clients by enabling Eve's schema endpoint. To do so, set the ``SCHEMA_ENDPOINT`` configuration option to the API endpoint name from which you want to serve schema data. Once enabled, Eve will treat the endpoint as a read only resource containing JSON encoded Cerberus -schema definitons, indexed by resource name. Resource visibility and +schema definitions, indexed by resource name. Resource visibility and authorization settings are honored, so internal resources or resources for which a request does not have read authentication will not be accessible at the schema endpoint. By default, ``SCHEMA_ENDPOINT`` is set to ``None``. @@ -2221,7 +2223,7 @@ Let's update the pipeline a little bit: } As you can see the `count` field is now going to sum the value of ``$value``, -which will be set by the client upon perfoming the request: +which will be set by the client upon performing the request: :: @@ -2234,7 +2236,7 @@ field/value pairs. Like with all other keywords, you can change ``aggregate`` to a keyword of your liking, just set ``QUERY_AGGREGATION`` in your settings. You can also set all options natively supported by PyMongo. For more -informations on aggregation see :ref:`datasource`. +information on aggregation see :ref:`datasource`. Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. From fc5ce75966c5b3748a0c0d4a0c528b6e653b0cbb Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 14:12:07 +0300 Subject: [PATCH 370/821] M-x whitespace-cleanup --- docs/features.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 8bea213ff..4fe7734e8 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1532,9 +1532,10 @@ notified of such a disastrous occurrence by hooking a callback function to the - ``on_delete_resource_originals`` for any resource hit by the request after having retrieved the originals documents. - ``on_delete_resource_originals_`` for the specific `` resource endpoint hit by the DELETE after having retrieved the original document. - -NOTE: those two event are useful in order to perform some business logic before the actual remove operation given the -look up and the list of originals + +NOTE: those two event are useful in order to perform some business +logic before the actual remove operation given the look up and the +list of originals .. _aggregation_hooks: From 929d221f9f7d18080cd3437a59135e1d3da20cf2 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 14:43:07 +0300 Subject: [PATCH 371/821] s/informations/information/ see: https://english.stackexchange.com/a/117553 --- CHANGES.rst | 2 +- docs/config.rst | 8 ++++---- eve/render.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d876927a4..a325b4728 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -790,7 +790,7 @@ Released on 28 September, 2015 - Fix: Replace the Cerberus rule ``keyschema``, now deprecated, with the new ``propertyschema`` (Julian Hille). - Fix: some error message are not filtered out of debug mode anymore, as they - are useful for users and do not leak informations. Closes #671 (Sebastien + are useful for users and do not leak information. Closes #671 (Sebastien Estienne). - Fix: reinforce Content-Type Header handling to avoid possible crash when it is missing (Sebastien Estienne). diff --git a/docs/config.rst b/docs/config.rst index 7aca19b49..9bcb9af47 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1086,7 +1086,7 @@ always lowercase. the endpoint, which is still accessible from the Eve data layer. See :ref:`internal_resources` for more - informations. Defaults to ``False``. + information. Defaults to ``False``. ``etag_ignore_fields`` List of fields that should not be used to compute the ETag value. @@ -1226,7 +1226,7 @@ defining the field validation rules. Allowed validation rules are: - ``decimal`` See :ref:`GeoJSON ` for more - informations geo fields. + information geo fields. ``required`` If ``True``, the field is mandatory on insertion. @@ -1439,7 +1439,7 @@ of the database collection. It is a dictionary with four allowed keys: ``'datasource': {'default_sort': [('name', 1)]}`` - For more informations on sort and filters see + For more information on sort and filters see :ref:`filters`. ``aggregation`` Aggregation pipeline and options. When used all @@ -1453,7 +1453,7 @@ of the database collection. It is a dictionary with four allowed keys: - ``pipeline``. The aggregation pipeline. Syntax must match the one supported by - PyMongo. For more informations see `PyMongo + PyMongo. For more information see `PyMongo Aggregation Examples`_ and the official `MongoDB Aggregation Framework`_ documentation. diff --git a/eve/render.py b/eve/render.py index 8babaf807..44f9ad09e 100644 --- a/eve/render.py +++ b/eve/render.py @@ -369,7 +369,7 @@ def render(self, data): @classmethod def xml_root_open(cls, data): """ Returns the opening tag for the XML root node. If the datastream - includes informations about resource endpoints (href, title), they will + includes information about resource endpoints (href, title), they will be added as node attributes. The resource endpoint is then removed to allow for further processing of the datastream. From 84478660957b0291b8ccdb6fcaa029bc7e9abf81 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 14:50:35 +0300 Subject: [PATCH 372/821] data_relation: wrong number of keys --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 9bcb9af47..48c5ee2e4 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1284,7 +1284,7 @@ defining the field validation rules. Allowed validation rules are: ``data_relation`` Allows to specify a referential integrity rule that the value must satisfy in order to - validate. It is a dict with three keys: + validate. It is a dict with four keys: - ``resource``: the name of the resource being referenced; - ``field``: the field name in the foreign resource; From 7da35159dcc6521a98a4e9a3c4ef92483da3ef60 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 15:04:29 +0300 Subject: [PATCH 373/821] Typos --- docs/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 48c5ee2e4..8303e5c53 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1425,7 +1425,7 @@ of the database collection. It is a dictionary with four allowed keys: ``filter`` Database query used to retrieve and validate data. If omitted, by default the whole - collection is retrievied. See :ref:`filter`. + collection is retrieved. See :ref:`filter`. ``projection`` Fieldset exposed by the endpoint. If omitted, by default all fields will be returned to the @@ -1532,7 +1532,7 @@ resource keyword allows you to redefine the fieldset. When you want to hide some *secret fields* from client, you should use inclusive projection setting and include all fields should be exposed. While, -when you want to limit default responsesto certain fields but still allow them +when you want to limit default responses to certain fields but still allow them to be accessible through client-side projections, you should use exclusive projection setting and exclude fields should be omitted. From 423cb2e8b682de82f62fd0db2eafebf2cd1452a8 Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Thu, 30 Aug 2018 15:04:37 +0300 Subject: [PATCH 374/821] more preferred -> preferred `preferred` already has the sense of more ... --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 8303e5c53..61786cd64 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1572,7 +1572,7 @@ The above will include all document fields but `username`. However, the following API call will return `username` this time. Thus, you can exploit this behaviour to serve media fields or other expensive fields. -In most cases, none or inclusive projection setting is more preferred. With +In most cases, none or inclusive projection setting is preferred. With inclusive projection, secret fields are taken care from server side, and default fields returned can be defined by short-cut functions from client-side. From 1ae138d8cbb41c8e66639710ba396d1e33f6cb82 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 09:40:30 +0200 Subject: [PATCH 375/821] Changelog for #1185 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a325b4728..eba1791df 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,7 +45,7 @@ Improved Docs ~~~~ -- Typos (`#1183`_, `#1184`_) +- Typos (`#1183`_, `#1184`_, `#1185`_) - Add ``MONGO_AUTH_SOURCE`` to Quickstart. (`#1168`_) - Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) @@ -55,6 +55,7 @@ Docs - Add a ``ISSUE_TEMPLATE.md`` GitHub template file. (`#1146`_) - Improve changelog format to reduce noise and increase readability. (`#1143`_) +.. _`#1185`: https://github.com/pyeve/eve/pull/1185 .. _`#1184`: https://github.com/pyeve/eve/pull/1184 .. _`#1183`: https://github.com/pyeve/eve/pull/1183 .. _`#1181`: https://github.com/pyeve/eve/issues/1181 From 70a774a872a130621baeb466c20812b7c03c5127 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 14:47:23 +0200 Subject: [PATCH 376/821] Use pyproject.toml with black --- .pre-commit-config.yaml | 1 - eve/flaskapp.py | 5 ++++- pyproject.toml | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c826d05a..367160623 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,6 @@ repos: rev: stable hooks: - id: black - args: [--quiet, --safe] python_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 42f76d2a1..4d423cfc4 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -291,7 +291,10 @@ def deprecated_renderers_settings(): """ Checks if JSON or XML setting is still being used instead of RENDERERS and if so, composes new settings. """ - msg = "{} setting is deprecated and will be removed" " in future release. Please use RENDERERS instead." + msg = ( + "{} setting is deprecated and will be removed" + " in future release. Please use RENDERERS instead." + ) if "JSON" in self.config or "XML" in self.config: self.config["RENDERERS"] = default_settings.RENDERERS[:] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..5ffb71086 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +safe = true +quiet = true From 7ae6a50073a92abf526a7e43a1e2d5649087d5a1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 16:58:30 +0200 Subject: [PATCH 377/821] v0.8.1 release date --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index eba1791df..bf95dae90 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. Version 0.8.1 ------------- -Unreleased +Released on October 4, 2018. New ~~~ From 1857b4f6dce8225348ff49f981904a84d8e88171 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 17:00:20 +0200 Subject: [PATCH 378/821] Bump version to 0.8.1 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 385ed5b54..ef835ada9 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.8.1.dev0" +__version__ = "0.8.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From c0e22494ec02598ff1f5e4d5335f4328877079f4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Oct 2018 17:04:41 +0200 Subject: [PATCH 379/821] Bump version to 0.8.2.dev0 --- CHANGES.rst | 5 +++++ eve/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bf95dae90..0095c5d01 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,11 @@ Eve Changelog Here you can see the full list of changes between each Eve release. +Version 0.8.2 +------------- + +- hic sunt leones + Version 0.8.1 ------------- diff --git a/eve/__init__.py b/eve/__init__.py index ef835ada9..9f7ca9ca6 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.8.1" +__version__ = "0.8.2.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From fa8b4f2ca401eae145014986be9157701fe5aff3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Oct 2018 10:49:37 +0200 Subject: [PATCH 380/821] Fix: CORS response headers missing for media endpoint Closes #1197. --- CHANGES.rst | 6 +++++- eve/endpoints.py | 5 ++++- eve/flaskapp.py | 2 +- eve/render.py | 2 ++ eve/tests/io/media.py | 27 +++++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0095c5d01..dff62afd3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,11 @@ Here you can see the full list of changes between each Eve release. Version 0.8.2 ------------- -- hic sunt leones +Fixed +~~~~~ +- CORS response headers missing for media endpoint (`#1197`_) + +.. _`#1197`: https://github.com/pyeve/eve/issues/1197 Version 0.8.1 ------------- diff --git a/eve/endpoints.py b/eve/endpoints.py index 923515300..e9f80358a 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -185,6 +185,9 @@ def media_endpoint(_id): .. versionadded:: 0.6 """ + if request.method == "OPTIONS": + return send_response(None, (None)) + file_ = app.media.get(_id) if file_ is None: return abort(404) @@ -238,7 +241,7 @@ def media_endpoint(_id): direct_passthrough=True, ) - return response + return send_response(None, (response,)) @requires_auth("resource") diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 4d423cfc4..8fb953fee 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -1067,7 +1067,7 @@ def _init_media_endpoint(self): self.config["MEDIA_URL"], ) self.add_url_rule( - media_url, "media", view_func=media_endpoint, methods=["GET"] + media_url, "media", view_func=media_endpoint, methods=["GET", "OPTIONS"] ) def _init_schema_endpoint(self): diff --git a/eve/render.py b/eve/render.py index 44f9ad09e..882c21da9 100644 --- a/eve/render.py +++ b/eve/render.py @@ -138,6 +138,8 @@ def _prepare_response( """ if request.method == "OPTIONS": resp = app.make_default_options_response() + elif isinstance(dct, Response): + resp = dct else: # obtain the best match between client's request and available mime # types, along with the corresponding render function. diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 2f2837405..4b5eed3ad 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -426,6 +426,33 @@ def test_gridfs_media_storage_base_url(self): url, ) + def test_media_endpoint_supports_CORS(self): + self.app._init_media_endpoint() + self.app.config["RETURN_MEDIA_AS_BASE64_STRING"] = False + self.app.config["RETURN_MEDIA_AS_URL"] = True + self.app.config["X_DOMAINS"] = "*" + + r, s = self._post() + self.assertEqual(STATUS_OK, r[STATUS]) + _id = r[self.id_field] + + with self.app.test_request_context(): + media_id = self.assertMediaStored(_id) + + methods = ["GET", "OPTIONS"] + for method in methods: + r = self.test_client.get( + "/media/%s" % media_id, + method=method, + headers=[("Origin", "http://example.com")], + ) + self.assert200(r.status_code) + self.assertEqual( + r.headers["Access-Control-Allow-Origin"], "http://example.com" + ) + self.assertEqual(r.headers["Vary"], "Origin") + self.assertTrue(method in r.headers["Access-Control-Allow-Methods"]) + def assertMediaField(self, _id, encoded, clean): # GET the file at the item endpoint r, s = self.parse_response(self.test_client.get("%s/%s" % (self.url, _id))) From ea3b4f581033424e856346ccc90b81b5ae434cd4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 20 Sep 2018 21:41:27 +0000 Subject: [PATCH 381/821] Pulled parsing for "sort" and "where" out of find() and into their own functions for reuse. --- eve/io/mongo/mongo.py | 92 +++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 99ea7318e..024279e3c 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -37,6 +37,57 @@ ) +def convert_sort_request_to_dict(req): + """ Converts the contents of a `ParsedRequest`'s `sort` property to + a dict + """ + client_sort = {} + if req and req.sort: + try: + # assume it's mongo syntax (ie. ?sort=[("name", 1)]) + client_sort = ast.literal_eval(req.sort) + except ValueError: + # it's not mongo so let's see if it's a comma delimited string + # instead (ie. "?sort=-age, name"). + sort = [] + for sort_arg in [s.strip() for s in req.sort.split(",")]: + if sort_arg[0] == "-": + sort.append((sort_arg[1:], -1)) + else: + sort.append((sort_arg, 1)) + if len(sort) > 0: + client_sort = sort + except Exception as e: + self.app.logger.exception(e) + abort(400, description=debug_error_message(str(e))) + return client_sort + + +def convert_where_request_to_dict(req): + """ Converts the contents of a `ParsedRequest`'s `where` property to + a dict + """ + query = {} + if req and req.where: + try: + query = self._sanitize(json.loads(req.where)) + except HTTPException as e: + # _sanitize() is raising an HTTP exception; let it fire. + raise + except: + # couldn't parse as mongo query; give the python parser a shot. + try: + query = parse(req.where) + except ParseError: + abort( + 400, + description=debug_error_message( + "Unable to parse `where` clause" + ), + ) + return query + + class MongoJSONEncoder(BaseJSONEncoder): """ Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. @@ -209,45 +260,8 @@ def find(self, resource, req, sub_resource_lookup): # TODO should validate on unknown sort fields (mongo driver doesn't # return an error) - client_sort = {} - spec = {} - - if req and req.sort: - try: - # assume it's mongo syntax (ie. ?sort=[("name", 1)]) - client_sort = ast.literal_eval(req.sort) - except ValueError: - # it's not mongo so let's see if it's a comma delimited string - # instead (ie. "?sort=-age, name"). - sort = [] - for sort_arg in [s.strip() for s in req.sort.split(",")]: - if sort_arg[0] == "-": - sort.append((sort_arg[1:], -1)) - else: - sort.append((sort_arg, 1)) - if len(sort) > 0: - client_sort = sort - except Exception as e: - self.app.logger.exception(e) - abort(400, description=debug_error_message(str(e))) - - if req and req.where: - try: - spec = self._sanitize(json.loads(req.where)) - except HTTPException as e: - # _sanitize() is raising an HTTP exception; let it fire. - raise - except: - # couldn't parse as mongo query; give the python parser a shot. - try: - spec = parse(req.where) - except ParseError: - abort( - 400, - description=debug_error_message( - "Unable to parse `where` clause" - ), - ) + client_sort = convert_sort_request_to_dict(req) + spec = convert_where_request_to_dict(req) bad_filter = validate_filters(spec, resource) if bad_filter: From 2c1988459534b2ced64c7c03468cbbc153712d22 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 20 Sep 2018 22:08:33 +0000 Subject: [PATCH 382/821] The previous commit was a bit hasty. Moved new functions inside Mongo class to allow them to access class attributes. --- eve/io/mongo/mongo.py | 104 +++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 024279e3c..b603aafbf 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -37,57 +37,6 @@ ) -def convert_sort_request_to_dict(req): - """ Converts the contents of a `ParsedRequest`'s `sort` property to - a dict - """ - client_sort = {} - if req and req.sort: - try: - # assume it's mongo syntax (ie. ?sort=[("name", 1)]) - client_sort = ast.literal_eval(req.sort) - except ValueError: - # it's not mongo so let's see if it's a comma delimited string - # instead (ie. "?sort=-age, name"). - sort = [] - for sort_arg in [s.strip() for s in req.sort.split(",")]: - if sort_arg[0] == "-": - sort.append((sort_arg[1:], -1)) - else: - sort.append((sort_arg, 1)) - if len(sort) > 0: - client_sort = sort - except Exception as e: - self.app.logger.exception(e) - abort(400, description=debug_error_message(str(e))) - return client_sort - - -def convert_where_request_to_dict(req): - """ Converts the contents of a `ParsedRequest`'s `where` property to - a dict - """ - query = {} - if req and req.where: - try: - query = self._sanitize(json.loads(req.where)) - except HTTPException as e: - # _sanitize() is raising an HTTP exception; let it fire. - raise - except: - # couldn't parse as mongo query; give the python parser a shot. - try: - query = parse(req.where) - except ParseError: - abort( - 400, - description=debug_error_message( - "Unable to parse `where` clause" - ), - ) - return query - - class MongoJSONEncoder(BaseJSONEncoder): """ Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. @@ -260,8 +209,8 @@ def find(self, resource, req, sub_resource_lookup): # TODO should validate on unknown sort fields (mongo driver doesn't # return an error) - client_sort = convert_sort_request_to_dict(req) - spec = convert_where_request_to_dict(req) + client_sort = self._convert_sort_request_to_dict(req) + spec = self._convert_where_request_to_dict(req) bad_filter = validate_filters(spec, resource) if bad_filter: @@ -884,6 +833,55 @@ def sanitize_keys(spec): return spec + def _convert_sort_request_to_dict(self, req): + """ Converts the contents of a `ParsedRequest`'s `sort` property to + a dict + """ + client_sort = {} + if req and req.sort: + try: + # assume it's mongo syntax (ie. ?sort=[("name", 1)]) + client_sort = ast.literal_eval(req.sort) + except ValueError: + # it's not mongo so let's see if it's a comma delimited string + # instead (ie. "?sort=-age, name"). + sort = [] + for sort_arg in [s.strip() for s in req.sort.split(",")]: + if sort_arg[0] == "-": + sort.append((sort_arg[1:], -1)) + else: + sort.append((sort_arg, 1)) + if len(sort) > 0: + client_sort = sort + except Exception as e: + self.app.logger.exception(e) + abort(400, description=debug_error_message(str(e))) + return client_sort + + def _convert_where_request_to_dict(self, req): + """ Converts the contents of a `ParsedRequest`'s `where` property to + a dict + """ + query = {} + if req and req.where: + try: + query = self._sanitize(json.loads(req.where)) + except HTTPException as e: + # _sanitize() is raising an HTTP exception; let it fire. + raise + except: + # couldn't parse as mongo query; give the python parser a shot. + try: + query = parse(req.where) + except ParseError: + abort( + 400, + description=debug_error_message( + "Unable to parse `where` clause" + ), + ) + return query + def _wc(self, resource): """ Syntactic sugar for the current collection write_concern setting. From b54af540dbdcdadd032870ed4b54be0d1044e1f2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 26 Jan 2019 11:10:26 +0100 Subject: [PATCH 383/821] Changelog for #1194 --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index dff62afd3..8dd531434 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,12 @@ Fixed ~~~~~ - CORS response headers missing for media endpoint (`#1197`_) +Improved +~~~~~~~~ +- Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving + their logic to dedicated methods (`#1194`_) + +.. _`1194`: https://github.com/pyeve/eve/pull/1194 .. _`#1197`: https://github.com/pyeve/eve/issues/1197 Version 0.8.1 From 124bddaba8898963d48e144de99654d105f1869b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 26 Jan 2019 11:12:01 +0100 Subject: [PATCH 384/821] Jeremy Solbrig --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 0c579ea8b..beae9c07e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,6 +72,7 @@ Patches and Contributions - Javier Gonel - Jean Boussier - Jen Montes +- Jeremy Solbrig - Joakim Uddholm - Johan Bloemberg - John Chang From 95a1cf5b20c86f669130504b7e01ade9d133fca4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 26 Jan 2019 11:13:57 +0100 Subject: [PATCH 385/821] fix broken changelog link --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8dd531434..2a3001552 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,7 +15,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) -.. _`1194`: https://github.com/pyeve/eve/pull/1194 +.. _`#1194`: https://github.com/pyeve/eve/pull/1194 .. _`#1197`: https://github.com/pyeve/eve/issues/1197 Version 0.8.1 From 484c91e9a7e79f5b9544a65fbf2ff98892304a9a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 26 Jan 2019 11:28:52 +0100 Subject: [PATCH 386/821] Fix flake8 issues on CI --- eve/endpoints.py | 2 +- eve/io/mongo/mongo.py | 2 +- eve/tests/renders.py | 2 +- eve/validation.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eve/endpoints.py b/eve/endpoints.py index e9f80358a..2a66dc057 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -204,7 +204,7 @@ def media_endpoint(_id): size = file_.length try: - m = re.search("(\d+)-(\d*)", range_header) + m = re.search(r"(\d+)-(\d*)", range_header) begin, end = m.groups() begin = int(begin) end = int(end) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b603aafbf..fa308bbea 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -866,7 +866,7 @@ def _convert_where_request_to_dict(self, req): if req and req.where: try: query = self._sanitize(json.loads(req.where)) - except HTTPException as e: + except HTTPException: # _sanitize() is raising an HTTP exception; let it fire. raise except: diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 423295645..ec72a35bb 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -198,7 +198,7 @@ def test_CORS(self): def test_CORS_regex(self): # test if X_DOMAINS_RE is set with a list of regexes, # origins are matched against this list (#974) - self.app.config["X_DOMAINS_RE"] = ["^http://sub-\d{3}\.domain\.com$"] + self.app.config["X_DOMAINS_RE"] = [r"^http://sub-\d{3}\.domain\.com$"] r = self.test_client.get("/", headers=[("Origin", "http://sub-123.domain.com")]) self.assert200(r.status_code) diff --git a/eve/validation.py b/eve/validation.py index 7ba88b155..aba7a2143 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -15,7 +15,7 @@ import copy import cerberus import cerberus.errors -from cerberus import DocumentError, SchemaError # flake8: noqa +from cerberus import DocumentError, SchemaError # noqa from eve.utils import config From a0be6c95ef2ad679f324290267b68a87f07668ee Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 26 Jan 2019 11:32:53 +0100 Subject: [PATCH 387/821] One last flake8 fix --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 41ece9353..4cb4e3ae0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -290,7 +290,7 @@ # fall back if theme is not there try: __import__("flask_theme_support") -except ImportError as e: +except ImportError: print("-" * 74) print("Warning: Flask themes unavailable. Building with default theme") print("If you want the Flask themes, run this command and build again:") From cf1c6bd175149bc229790bf0de7bd14920c63ba4 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Thu, 11 Oct 2018 20:59:06 -0500 Subject: [PATCH 388/821] add data relation hateoas to response json --- eve/methods/common.py | 62 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 6c0980458..3ee2b5fc6 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -638,6 +638,9 @@ def build_response_document(document, resource, embedded_fields, latest_doc=None elif "self" not in document[config.LINKS]: document[config.LINKS].update(self_dict) + # add data relation links if hateoas enabled + resolve_data_relation_links(document, resource) + # add version numbers resolve_document_version(document, resource, "GET", latest_doc) @@ -681,12 +684,69 @@ def field_definition(resource, chained_fields): definition = definition["schema"][field] field_type = definition.get("type") if field_type == "list": - definition = definition["schema"] + # the list can be 1) a list of allowed values for string and list types + # 2) a list of references that have schema + # we want to resolve field definition deeper for the second one + definition = definition.get("schema", definition) elif field_type == "objectid": pass return definition +def resolve_data_relation_links(document, resource): + """ Resolves all fields in a document that has data relation to other resources + + :param document: the document to include data relation link. + :param resource: the resource name. + + .. versionadded:: 0.8.2 + """ + resource_def = config.DOMAIN[resource] + related_dict = {} + + for field in resource_def.get("schema", {}): + + field_def = field_definition(resource, field) + if "data_relation" not in field_def: + continue + + if field in document and document[field] is not None: + # Get the resource endpoint string for the linked relation + related_resource = ( + document[field].collection + if isinstance(document[field], DBRef) + else field_def["data_relation"]["resource"] + ) + + # Get the item endpoint id for the linked relation + related_document_id = document[field] + if isinstance(related_document_id, DBRef): + related_document_id = related_document_id.id + if isinstance(related_document_id, dict): + related_resource_field = field_definition(resource, field)[ + "data_relation" + ]["field"] + related_document_id = related_document_id[related_resource_field] + + # Get the version for the endpoint + related_version = ( + document[field].get("_version") + if isinstance(document[field], dict) + else None + ) + + related_dict.update( + { + field: document_link( + related_resource, related_document_id, related_version + ) + } + ) + + if related_dict != {}: + document[config.LINKS].update({"related": related_dict}) + + def resolve_embedded_fields(resource, req): """ Returns a list of validated embedded fields from the incoming request or from the resource definition is the request does not specify. From e216f9c5893410e500afb459987747499058af1c Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 12 Oct 2018 18:56:41 -0500 Subject: [PATCH 389/821] add xml rendering for data relation links --- eve/methods/common.py | 69 +++++++++++++++++++++++++------------------ eve/render.py | 63 +++++++++++++++++++++++++++++++++++---- 2 files changed, 98 insertions(+), 34 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 3ee2b5fc6..dea5c5af7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -668,6 +668,9 @@ def field_definition(resource, chained_fields): :param resource: the resource name whose field to be accepted. :param chained_fields: query string to retrieve field definition + .. versionchanged:: 0.8.2 + fix field definition of list without a schema. See #1204. + .. versionadded 0.5 """ definition = config.DOMAIN[resource] @@ -696,7 +699,7 @@ def field_definition(resource, chained_fields): def resolve_data_relation_links(document, resource): """ Resolves all fields in a document that has data relation to other resources - :param document: the document to include data relation link. + :param document: the document to include data relation links. :param resource: the resource name. .. versionadded:: 0.8.2 @@ -711,37 +714,47 @@ def resolve_data_relation_links(document, resource): continue if field in document and document[field] is not None: - # Get the resource endpoint string for the linked relation - related_resource = ( - document[field].collection - if isinstance(document[field], DBRef) - else field_def["data_relation"]["resource"] - ) + related_links = [] - # Get the item endpoint id for the linked relation - related_document_id = document[field] - if isinstance(related_document_id, DBRef): - related_document_id = related_document_id.id - if isinstance(related_document_id, dict): - related_resource_field = field_definition(resource, field)[ - "data_relation" - ]["field"] - related_document_id = related_document_id[related_resource_field] - - # Get the version for the endpoint - related_version = ( - document[field].get("_version") - if isinstance(document[field], dict) - else None - ) + # Make the code DRY for list of linked relation and single linked relation + for related_document_id in ( + document[field] + if isinstance(document[field], list) + else [document[field]] + ): + # Get the resource endpoint string for the linked relation + related_resource = ( + related_document_id.collection + if isinstance(related_document_id, DBRef) + else field_def["data_relation"]["resource"] + ) + + # Get the item endpoint id for the linked relation + if isinstance(related_document_id, DBRef): + related_document_id = related_document_id.id + if isinstance(related_document_id, dict): + related_resource_field = field_definition(resource, field)[ + "data_relation" + ]["field"] + related_document_id = related_document_id[related_resource_field] + + # Get the version for the item endpoint id + related_version = ( + related_document_id.get("_version") + if isinstance(related_document_id, dict) + else None + ) - related_dict.update( - { - field: document_link( + related_links.append( + document_link( related_resource, related_document_id, related_version ) - } - ) + ) + + if isinstance(document[field], list): + related_dict.update({field: related_links}) + else: + related_dict.update({field: related_links[0]}) if related_dict != {}: document[config.LINKS].update({"related": related_dict}) diff --git a/eve/render.py b/eve/render.py index 882c21da9..2636e7dae 100644 --- a/eve/render.py +++ b/eve/render.py @@ -418,11 +418,15 @@ def xml_add_meta(cls, data): @classmethod def xml_add_links(cls, data): """ Returns as many nodes as there are in the datastream. The - links are then removed from the datastream to allow for further + added links are then removed from the datastream to allow for further processing. :param data: the data stream to be rendered as xml. + .. versionchanged:: 0.8.2 + Keep data relation links in the datastream as they will be + processed as node attributes in xml_dict + .. versionchanged:: 0.5 Always return ordered items (#441). @@ -436,7 +440,12 @@ def xml_add_links(cls, data): links = data.pop(config.LINKS, {}) ordered_links = OrderedDict(sorted(links.items())) for rel, link in ordered_links.items(): - if isinstance(link, list): + if rel == "related": + # add data relation links back for + # future processing of hateoas attributes + data.update({config.LINKS: {rel: link}}) + + elif isinstance(link, list): xml += "".join( [ chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) @@ -491,6 +500,9 @@ def xml_dict(cls, data): :param data: the data stream to be rendered as xml. + .. versionchanged:: 0.8.2 + Renders hateoas attributes on XML nodes. See #1204. + .. versionchanged:: 0.5 Always return ordered items (#441). @@ -500,6 +512,7 @@ def xml_dict(cls, data): .. versionadded:: 0.0.3 """ xml = "" + related_links = data.pop(config.LINKS, {}).pop("related", {}) ordered_items = OrderedDict(sorted(data.items())) for k, v in ordered_items.items(): if isinstance(v, datetime.datetime): @@ -508,13 +521,51 @@ def xml_dict(cls, data): v = v.isoformat() if not isinstance(v, list): v = [v] - for value in v: + for idx, value in enumerate(v): if isinstance(value, dict): links = cls.xml_add_links(value) - xml += "<%s>" % k + xml += cls.xml_field_open(k, idx, related_links) xml += cls.xml_dict(value) xml += links - xml += "" % k + xml += cls.xml_field_close(k) else: - xml += "<%s>%s" % (k, utils.escape(value), k) + xml += cls.xml_field_open(k, idx, related_links) + xml += "%s" % utils.escape(value) + xml += cls.xml_field_close(k) return xml + + @classmethod + def xml_field_open(cls, field, idx, related_links): + """ Returns opening tag for XML field element node. + + :param field: field name for the element node + :param idx: the index in the data relation links if serializing a list of same field to XML + :param related_links: a dictionary that stores all data relation links + + .. versionadded:: 0.8.2 + """ + if field in related_links: + if isinstance(related_links[field], list): + return '<%s href="%s" title="%s">' % ( + field, + related_links[field][idx]["href"], + related_links[field][idx]["title"], + ) + else: + return '<%s href="%s" title="%s">' % ( + field, + related_links[field]["href"], + related_links[field]["title"], + ) + else: + return "<%s>" % field + + @classmethod + def xml_field_close(cls, field): + """ Returns closing tag of XML field element node. + + :param field: field name for the element node + + .. versionadded:: 0.8.2 + """ + return "" % field From 4b1ad6cdea095a8d6de91d8ea2b9dabb474ea9d3 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Sun, 14 Oct 2018 18:42:33 -0500 Subject: [PATCH 390/821] add tests for data relation hateoas links --- docs/features.rst | 2 +- eve/methods/common.py | 27 +++++++++++++++--- eve/methods/get.py | 10 +++++-- eve/render.py | 4 +-- eve/tests/__init__.py | 13 +++++++++ eve/tests/methods/get.py | 59 +++++++++++++++++++++++++++++++++++++++- eve/tests/renders.py | 27 ++++++++++++++++++ 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 4fe7734e8..f1b7a2906 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -478,7 +478,7 @@ HATEOAS links are always relative to the API entry point, so if your API home is at ``examples.com/api/v1``, the ``self`` link in the above example would mean that the *people* endpoint is located at ``examples.com/api/v1/people``. -Please note that ``next``, ``previous`` and ``last`` items will only be +Please note that ``next``, ``previous``, ``last`` and ``related`` items will only be included when appropriate. Disabling HATEOAS diff --git a/eve/methods/common.py b/eve/methods/common.py index dea5c5af7..28a9de58d 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -9,6 +9,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import re import base64 import time from copy import copy @@ -603,6 +604,9 @@ def build_response_document(document, resource, embedded_fields, latest_doc=None :param embedded_fields: the list of fields we are allowed to embed. :param document: the latest version of document. + .. versionchanged:: 0.8.2 + Add data relation fields hateoas support (#1204). + .. versionchanged:: 0.5 Only compute ETAG if necessary (#369). Add version support (#475). @@ -669,7 +673,7 @@ def field_definition(resource, chained_fields): :param chained_fields: query string to retrieve field definition .. versionchanged:: 0.8.2 - fix field definition of list without a schema. See #1204. + fix field definition for list without a schema. See #1204. .. versionadded 0.5 """ @@ -1334,6 +1338,9 @@ def document_link(resource, document_id, version=None): :param document_id: the document unique identifier. :param version: the document version. Defaults to None. + .. versionchanged:: 0.8.2 + Support document link for data relation resources. See #1204. + .. versionchanged:: 0.5 Add version support (#475). @@ -1349,17 +1356,23 @@ def document_link(resource, document_id, version=None): version_part = "?version=%s" % version if version else "" return { "title": "%s" % config.DOMAIN[resource]["item_title"], - "href": "%s/%s%s" % (resource_link(), document_id, version_part), + "href": "%s/%s%s" % (resource_link(resource), document_id, version_part), } -def resource_link(): +def resource_link(resource=None): """ Returns the current resource path relative to the API entry point. Mostly going to be used by hateoas functions when building document/resource links. The resource URL stored in the config settings might contain regexes and custom variable names, all of which are not needed in the response payload. + :param resource: the resource name if not using the resource from request.path + + .. versionchanged:: 0.8.2 + Support resource link for data relation resources + which may be different from request.path resource. See #1204. + .. versionchanged:: 0.5 URL is relative to API root. @@ -1377,7 +1390,13 @@ def strip_prefix(hit): path = strip_prefix(config.URL_PREFIX + "/") if config.API_VERSION: path = strip_prefix(config.API_VERSION + "/") - return path + + # If request path does not match resource URL regex definition + # We are creating a path for data relation resources + if resource and not re.search(config.DOMAIN[resource]["url"], path): + return config.DOMAIN[resource]["url"] + else: + return path def oplog_push(resource, document, op, id=None): diff --git a/eve/methods/get.py b/eve/methods/get.py index a444e10bb..f068e813b 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -279,6 +279,10 @@ def getitem_internal(resource, **lookup): :param resource: the name of the resource to which the document belongs. :param **lookup: the lookup query. + .. versionchanged:: 0.8.2 + Prevent extra hateoas links from overwriting + already existed data relation hateoas links. + .. versionchanged:: 0.6 Handle soft deleted documents @@ -464,8 +468,10 @@ def getitem_internal(resource, **lookup): if config.DOMAIN[resource]["pagination"]: response[config.META] = _meta_links(req, count) else: - response[config.LINKS] = _pagination_links( - resource, req, None, response[resource_def["id_field"]] + response[config.LINKS].update( + _pagination_links( + resource, req, None, response[resource_def["id_field"]] + ) ) # callbacks not supported on version diffs because of partial documents diff --git a/eve/render.py b/eve/render.py index 2636e7dae..9662ebb1e 100644 --- a/eve/render.py +++ b/eve/render.py @@ -548,13 +548,13 @@ def xml_field_open(cls, field, idx, related_links): if isinstance(related_links[field], list): return '<%s href="%s" title="%s">' % ( field, - related_links[field][idx]["href"], + utils.escape(related_links[field][idx]["href"]), related_links[field][idx]["title"], ) else: return '<%s href="%s" title="%s">' % ( field, - related_links[field]["href"], + utils.escape(related_links[field]["href"]), related_links[field]["title"], ) else: diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 8f4bb3e59..badc531fd 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -311,6 +311,19 @@ def assertLastLink(self, links, page): else: self.assertTrue("last" not in links) + def assertRelatedLink(self, links, field): + self.assertTrue("related" in links) + data_relation_links = links["related"] + self.assertTrue(field in data_relation_links) + related_field_links = data_relation_links[field] + for related_field_link in ( + related_field_links + if isinstance(related_field_links, list) + else [related_field_links] + ): + self.assertTrue("title" in related_field_link) + self.assertTrue("href" in related_field_link) + def assertCustomParams(self, link, params): self.assertTrue("href" in link) url_params = parse_qs(urlparse(link["href"]).query) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index cb4a03e9d..96af25355 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -4,6 +4,7 @@ import simplejson as json from datetime import datetime, timedelta from bson import ObjectId +from bson.dbref import DBRef from bson.son import SON from werkzeug.datastructures import ImmutableMultiDict from eve.tests import TestBase @@ -1579,7 +1580,7 @@ def assertItemResponse(self, response, status, resource=None): self.assert200(status) self.assertTrue(self.app.config["ETAG"] in response) links = response["_links"] - self.assertEqual(len(links), 3) + self.assertTrue(len(links) == 3 or len(links) == 4) self.assertHomeLink(links) self.assertCollectionLink(links, resource or self.known_resource) self.assertItem(response, resource or self.known_resource) @@ -1818,6 +1819,62 @@ def test_subresource_getitem(self): self.assertEqual(response["person"], str(fake_contact_id)) self.assertEqual(response["_id"], self.invoice_id) + def test_getitem_data_relation_hateoas(self): + # We need to assign a `person` to our test invoice + _db = self.connection[MONGO_DBNAME] + + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + url = self.domain[self.known_resource]["url"] + item_title = self.domain[self.known_resource]["item_title"] + invoices = self.domain["invoices"] + + # Test nullable data relation fields + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": None}} + ) + + response, status = self.get("%s/%s" % (invoices["url"], self.invoice_id)) + self.assertTrue("related" not in response["_links"]) + + # Test object id data relation fields + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) + + response, status = self.get("%s/%s" % (invoices["url"], self.invoice_id)) + self.assertRelatedLink(response["_links"], "person") + related_links = response["_links"]["related"] + self.assertEqual(related_links["person"]["title"], item_title) + self.assertEqual( + related_links["person"]["href"], "%s/%s" % (url, fake_contact_id) + ) + + # Test DBRef data relation fields + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, + {"$set": {"persondbref": DBRef("contacts", fake_contact_id)}}, + ) + + response, status = self.get("%s/%s" % (invoices["url"], self.invoice_id)) + self.assertRelatedLink(response["_links"], "persondbref") + related_links = response["_links"]["related"] + self.assertEqual(related_links["persondbref"]["title"], item_title) + self.assertEqual( + related_links["persondbref"]["href"], "%s/%s" % (url, fake_contact_id) + ) + + # Test list of object id data relation fields + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, + {"$set": {"invoicing_contacts": [fake_contact_id] * 5}}, + ) + + response, status = self.get("%s/%s" % (invoices["url"], self.invoice_id)) + self.assertRelatedLink(response["_links"], "invoicing_contacts") + related_links = response["_links"]["related"] + self.assertEqual(len(related_links["invoicing_contacts"]), 5) + def test_getitem_ifmatch_disabled(self): # when IF_MATCH is disabled no etag is present in payload self.app.config["IF_MATCH"] = False diff --git a/eve/tests/renders.py b/eve/tests/renders.py index ec72a35bb..18056ef17 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from bson import ObjectId from eve.tests import TestBase from eve.utils import api_prefix from eve.tests.test_settings import MONGO_DBNAME @@ -63,6 +64,32 @@ def test_xml_ordered_nodes(self): idx3 = data.index(b"parent") self.assertTrue(idx1 < idx2 < idx3) + def test_xml_data_relation_hateoas(self): + # We need to assign a `person` to our test invoice + _db = self.connection[MONGO_DBNAME] + + fake_contact = self.random_contacts(1)[0] + fake_contact_id = _db.contacts.insert_one(fake_contact).inserted_id + url = self.domain[self.known_resource]["url"] + item_title = self.domain[self.known_resource]["item_title"] + invoices = self.domain["invoices"] + + # Test object id data relation fields + _db.invoices.update_one( + {"_id": ObjectId(self.invoice_id)}, {"$set": {"person": fake_contact_id}} + ) + + r = self.test_client.get( + "%s/%s" % (invoices["url"], self.invoice_id), + headers=[("Accept", "application/xml")], + ) + data_relation_opening_tag = '' % ( + url, + fake_contact_id, + item_title, + ) + self.assertTrue(data_relation_opening_tag in r.data.decode()) + def test_unknown_render(self): r = self.test_client.get("/", headers=[("Accept", "application/html")]) self.assertEqual(r.content_type, "application/json") From 77b710e3d7eb3cd12a34de791be522e9a3ee0172 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Jan 2019 16:24:20 +0100 Subject: [PATCH 391/821] Changelog for #1024 --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2a3001552..3f41aac6e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,9 +12,15 @@ Fixed Improved ~~~~~~~~ +- HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary + inside, and each key-value pair yields the related links for a data relation + field (`#1204`_) +- XML renderer now supports data field tag attributes such as ``href`` and + ``title`` (`#1204`_) - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1204`: https://github.com/pyeve/eve/pull/1204 .. _`#1194`: https://github.com/pyeve/eve/pull/1194 .. _`#1197`: https://github.com/pyeve/eve/issues/1197 From dafd25a2a56f82d512b2116ea0007ffa8fa22779 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Jan 2019 16:25:26 +0100 Subject: [PATCH 392/821] Shaoyu Meng --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index beae9c07e..33dfa35fa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -154,6 +154,7 @@ Patches and Contributions - Sebastien Estienne - Sebastián Magrí - Serge Kir +- Shaoyu Meng - Simon Schönfeld - Sobolev Nikita - Stanislav Filin From 9c1b71a22b39c3f1a1f8382b67395112bb938d52 Mon Sep 17 00:00:00 2001 From: Vincent Bisserie Date: Tue, 6 Nov 2018 11:31:23 +0100 Subject: [PATCH 393/821] Filter on attributes of oplog entry Signed-off-by: Vincent Bisserie --- eve/methods/common.py | 13 ++++++------- eve/tests/methods/common.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 28a9de58d..91c678de0 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1471,13 +1471,12 @@ def oplog_push(resource, document, op, id=None): entry["u"] = auth.get_user_or_token() if auth else "n/a" if op in config.OPLOG_CHANGE_METHODS: - # these fields are already contained in 'entry'. - del (update[config.LAST_UPDATED]) - # legacy documents (v0.4 or less) could be missing the etag - # field - if config.ETAG in update: - del (update[config.ETAG]) - entry["c"] = update + entry["c"] = { + key: value + for key, value in update.items() + # these fields are already contained in 'entry'. + if key not in [config.ETAG, config.LAST_UPDATED] + } else: pass diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 4d3e195bf..522efbd4d 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -604,6 +604,23 @@ def test_post_oplog(self): self.assertOpLogEntry(oplog_entry, "POST") self.assertTrue("extra" not in oplog_entry) + def test_post_oplog_does_not_alter_document(self): + """ Make sure we don't alter document ETag when performing an + oplog_push. See #590 and #1206. """ + self.app.config["OPLOG_CHANGE_METHODS"].append("POST") + r = self.test_client.post( + self.different_resource_url, + data=json.dumps({"username": "test", "ref": "1234567890123456789012345"}), + headers=self.headers, + environ_base={"REMOTE_ADDR": "127.0.0.1"}, + ) + + item_id = json.loads(r.get_data())["_id"] + etag1 = json.loads(r.get_data())["_etag"] + item, _ = self.get(self.different_resource, item=item_id) + etag2 = item["_etag"] + self.assertEqual(etag1, etag2) + def test_patch_oplog(self): self.headers.append(("If-Match", self.item_etag)) r = self.test_client.patch( From 03414468d2351e46ee015fc8bfa58cd1cb0976df Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Jan 2019 16:52:21 +0100 Subject: [PATCH 394/821] Changelog for #1207 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3f41aac6e..803ae9575 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Version 0.8.2 Fixed ~~~~~ +- Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) Improved @@ -20,6 +21,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1206`: https://github.com/pyeve/eve/issues/1206 .. _`#1204`: https://github.com/pyeve/eve/pull/1204 .. _`#1194`: https://github.com/pyeve/eve/pull/1194 .. _`#1197`: https://github.com/pyeve/eve/issues/1197 From 466deab84ec5f7ffe81fc105548b1b95333d701c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Jan 2019 16:53:22 +0100 Subject: [PATCH 395/821] Vincent Bisserie --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 33dfa35fa..ad9775b18 100644 --- a/AUTHORS +++ b/AUTHORS @@ -167,6 +167,7 @@ Patches and Contributions - Tomasz Jezierski - Valerie Coffman - Vasilis Lolis +- Vincent Bisserie - Wael M. Nasreddine - Wan Bachtiar - Wei Guan From c13f97dcf8ad860e0acc2f2ed0080f66ea0f6783 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Thu, 22 Nov 2018 17:18:37 +0800 Subject: [PATCH 396/821] Prune the stages from aggregation pipeline whose condition is not set or empty. --- eve/methods/get.py | 30 ++++++++++++++++++-- eve/tests/methods/get.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index f068e813b..2c4a76769 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -156,6 +156,23 @@ def parse_again(st_value, key, value): else: parse_again(st_value, key, value) + def prune_aggregation_stage(d): + """ + Remove the stages whose parameters are not set. + + For example, we have endpoint with a stage like {'$lookup': {'$userId': '$a', '$name': '$b'}}, $a is provided + but $b is not provided. Then the stage will be pruned as {'$lookup': {'$userId': '$a'}} + """ + for st_key, st_value in d.items(): + if isinstance(st_value, dict): + prune_aggregation_stage(st_value) + if len(st_value.keys()) == 0: + # remove the key: value when value is an empty dict + del d[st_key] + if st_value[0] == '$': + # remove the key: value when value is not replaced + del d[st_key] + response = {} documents = [] req = parse_request(resource) @@ -173,15 +190,22 @@ def parse_again(st_value, key, value): for stage in req_pipeline: parse_aggregation_stage(stage, key, value) + # remove the stages whose conditions are not yet set + req_pipeline_pruned = [] + for stage in req_pipeline: + prune_aggregation_stage(stage) + if len(stage.keys()) > 0: + req_pipeline_pruned.append(stage) + if req.max_results > 1: limit = {"$limit": req.max_results} skip = {"$skip": (req.page - 1) * req.max_results} - req_pipeline.append(skip) - req_pipeline.append(limit) + req_pipeline_pruned.append(skip) + req_pipeline_pruned.append(limit) getattr(app, "before_aggregation")(resource, req_pipeline) - cursor = app.data.aggregate(resource, req_pipeline, options) + cursor = app.data.aggregate(resource, req_pipeline_pruned, options) for document in cursor: documents.append(document) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 96af25355..a545e6029 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1465,6 +1465,67 @@ def test_get_aggregation_with_lists(self): docs = response["_items"] self.assertEqual(len(docs), 1) + + def test_get_aggregation_pruning(self): + + date = datetime.utcnow() + + _db = self.connection[MONGO_DBNAME] + _db.aggregate_test.insert_many( + [ + {"x": 1, "date": date}, + {"x": 2, "date": date}, + {"x": 3, "date": date}, + {"x": 4, "date": date + timedelta(days=-1)}, + self.app.register_resource( + 'aggregate_test', { + 'datasource': { + 'aggregation': { + 'pipeline': [ + {"$match": {"date": {"$gte": "$date"}, "x": "$x"}} + ], + } + } + } + ) + + # look for date = now, x = 4, which shall return empty result + challenge = date.strftime(self.app.config['DATE_FORMAT']) + response, status = self.get('aggregate_test?aggregate={"$date": "%s", "$x": 4}' + % challenge) + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 0) + + # look for date = yesterday, x = 4, which shall return only one result + challenge = (date + timedelta(days=-1)).strftime( + self.app.config['DATE_FORMAT']) + response, status = self.get('aggregate_test?aggregate={"$date": "%s", "$x": 4}' + % challenge) + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]['x'], 4) + + # look for date = yesterday, which shall return all four results + challenge = (date + timedelta(days=-1)).strftime( + self.app.config['DATE_FORMAT']) + response, status = self.get('aggregate_test?aggregate={"$date": "%s"}' + % challenge) + + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 4) + + # look for x = 3, which shall return only one result + response, status = self.get('aggregate_test?aggregate={"x": 3}') + + self.assert200(status) + docs = response['_items'] + self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]['x'], 3) +>>>>>>> Prune the stages from aggregation pipeline whose condition is not set or empty. + def test_get_aggregation_pagination(self): _db = self.connection[MONGO_DBNAME] From cae0d0ad18f9fa54e3744d2548e7eecc3d04c3cf Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 11:34:57 +0800 Subject: [PATCH 397/821] apply black to reformat the files --- eve/flaskapp.py | 2 +- eve/io/base.py | 2 +- eve/io/mongo/mongo.py | 2 +- eve/methods/common.py | 2 +- eve/methods/delete.py | 2 +- eve/methods/get.py | 2 +- eve/methods/put.py | 2 +- eve/tests/methods/get.py | 92 ++++++++++++++++++++++++++++---------- eve/tests/methods/patch.py | 2 +- eve/tests/methods/put.py | 4 +- 10 files changed, 78 insertions(+), 34 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 8fb953fee..6c7926b08 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -142,7 +142,7 @@ def __init__( url_converters=None, json_encoder=None, media=GridFSMediaStorage, - **kwargs + **kwargs, ): """ Eve main WSGI app is implemented as a Flask subclass. Since we want to be able to launch our API by simply invoking Flask's run() method, diff --git a/eve/io/base.py b/eve/io/base.py index 650b5fc65..76cfaaf1c 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -161,7 +161,7 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, - **lookup + **lookup, ): """ Retrieves a single document/record. Consumed when a request hits an item endpoint (`/people/id/`). diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index fa308bbea..bb7c27a6b 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -257,7 +257,7 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, - **lookup + **lookup, ): """ Retrieves a single document. diff --git a/eve/methods/common.py b/eve/methods/common.py index 91c678de0..ad9b840c7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -40,7 +40,7 @@ def get_document( original=None, check_auth_value=True, force_auth_field_projection=False, - **lookup + **lookup, ): """ Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client diff --git a/eve/methods/delete.py b/eve/methods/delete.py index ad354203a..929fd5260 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -234,7 +234,7 @@ def delete(resource, **lookup): concurrency_check=False, suppress_callbacks=True, original=document, - **lookup + **lookup, ) else: # TODO if the resource schema includes media files, these won't be diff --git a/eve/methods/get.py b/eve/methods/get.py index 2c4a76769..6a4b28c4f 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -169,7 +169,7 @@ def prune_aggregation_stage(d): if len(st_value.keys()) == 0: # remove the key: value when value is an empty dict del d[st_key] - if st_value[0] == '$': + if st_value[0] == "$": # remove the key: value when value is not replaced del d[st_key] diff --git a/eve/methods/put.py b/eve/methods/put.py index 603551504..7fd4bd55c 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -140,7 +140,7 @@ def put_internal( concurrency_check, check_auth_value=False, force_auth_field_projection=True, - **lookup + **lookup, ) if not original: if config.UPSERT_ON_PUT: diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index a545e6029..0aa615fda 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1478,53 +1478,97 @@ def test_get_aggregation_pruning(self): {"x": 3, "date": date}, {"x": 4, "date": date + timedelta(days=-1)}, self.app.register_resource( - 'aggregate_test', { - 'datasource': { - 'aggregation': { - 'pipeline': [ - {"$match": {"date": {"$gte": "$date"}, "x": "$x"}} - ], + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [{"$match": {"date": {"$gte": "$date"}, "x": "$x"}}] } } - } + }, ) # look for date = now, x = 4, which shall return empty result - challenge = date.strftime(self.app.config['DATE_FORMAT']) - response, status = self.get('aggregate_test?aggregate={"$date": "%s", "$x": 4}' - % challenge) + challenge = date.strftime(self.app.config["DATE_FORMAT"]) + response, status = self.get( + 'aggregate_test?aggregate={"$date": "%s", "$x": 4}' % challenge + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 0) # look for date = yesterday, x = 4, which shall return only one result - challenge = (date + timedelta(days=-1)).strftime( - self.app.config['DATE_FORMAT']) - response, status = self.get('aggregate_test?aggregate={"$date": "%s", "$x": 4}' - % challenge) + challenge = (date + timedelta(days=-1)).strftime(self.app.config["DATE_FORMAT"]) + response, status = self.get( + 'aggregate_test?aggregate={"$date": "%s", "$x": 4}' % challenge + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 1) - self.assertEqual(docs[0]['x'], 4) + self.assertEqual(docs[0]["x"], 4) # look for date = yesterday, which shall return all four results - challenge = (date + timedelta(days=-1)).strftime( - self.app.config['DATE_FORMAT']) - response, status = self.get('aggregate_test?aggregate={"$date": "%s"}' - % challenge) + challenge = (date + timedelta(days=-1)).strftime(self.app.config["DATE_FORMAT"]) + response, status = self.get( + 'aggregate_test?aggregate={"$date": "%s"}' % challenge + ) self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 4) # look for x = 3, which shall return only one result response, status = self.get('aggregate_test?aggregate={"x": 3}') self.assert200(status) - docs = response['_items'] + docs = response["_items"] self.assertEqual(len(docs), 1) self.assertEqual(docs[0]['x'], 3) ->>>>>>> Prune the stages from aggregation pipeline whose condition is not set or empty. + + def test_get_aggregation_with_lists(self): + _db = self.connection[MONGO_DBNAME] + _db.aggregate_test.insert_many( + [ + {"x": 1, "tags": ["a", "b", "c"]}, + {"x": 2, "tags": ["a"]}, + {"x": 3, "tags": ["a", "b"]}, + {"x": [4], "tags": []}, + ] + ) + + self.app.register_resource( + "aggregate_test", + { + "datasource": { + "aggregation": { + "pipeline": [ + { + "$match": { + "$or": [{"tags": "$match_tags"}, {"x": ["$x"]}] + } + } + ] + } + } + }, + ) + + response, status = self.get('aggregate_test?aggregate={"$match_tags": "a"}') + self.assert200(status) + docs = response["_items"] + self.assertEqual(len(docs), 3) + + response, status = self.get( + 'aggregate_test?aggregate={"$match_tags": ["a", "b"]}' + ) + self.assert200(status) + docs = response["_items"] + self.assertEqual(len(docs), 1) + + response, status = self.get('aggregate_test?aggregate={"$x": 4}') + self.assert200(status) + docs = response["_items"] + self.assertEqual(len(docs), 1) def test_get_aggregation_pagination(self): _db = self.connection[MONGO_DBNAME] diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 638b71568..482a50608 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -235,7 +235,7 @@ def test_patch_internal(self): self.known_resource, data, concurrency_check=False, - **{"_id": self.item_id} + **{"_id": self.item_id}, ) db_value = self.compare_patch_with_get(test_field, r) self.assertEqual(db_value, test_value) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index 43e60990b..a39719a59 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -352,7 +352,7 @@ def test_put_internal(self): self.known_resource, data, concurrency_check=False, - **{"_id": self.item_id} + **{"_id": self.item_id}, ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) @@ -369,7 +369,7 @@ def test_put_internal_skip_validation(self): data, concurrency_check=False, skip_validation=True, - **{"_id": self.item_id} + **{"_id": self.item_id}, ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) From 4a63943791104525f4451b47fb11dc10de123f25 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 13:00:58 +0800 Subject: [PATCH 398/821] Revert "apply black to reformat the files" This reverts commit 15d90e0 --- eve/flaskapp.py | 2 +- eve/io/base.py | 2 +- eve/io/mongo/mongo.py | 2 +- eve/methods/common.py | 2 +- eve/methods/delete.py | 2 +- eve/methods/put.py | 2 +- eve/tests/methods/patch.py | 2 +- eve/tests/methods/put.py | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 6c7926b08..8fb953fee 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -142,7 +142,7 @@ def __init__( url_converters=None, json_encoder=None, media=GridFSMediaStorage, - **kwargs, + **kwargs ): """ Eve main WSGI app is implemented as a Flask subclass. Since we want to be able to launch our API by simply invoking Flask's run() method, diff --git a/eve/io/base.py b/eve/io/base.py index 76cfaaf1c..650b5fc65 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -161,7 +161,7 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, - **lookup, + **lookup ): """ Retrieves a single document/record. Consumed when a request hits an item endpoint (`/people/id/`). diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index bb7c27a6b..fa308bbea 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -257,7 +257,7 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, - **lookup, + **lookup ): """ Retrieves a single document. diff --git a/eve/methods/common.py b/eve/methods/common.py index ad9b840c7..91c678de0 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -40,7 +40,7 @@ def get_document( original=None, check_auth_value=True, force_auth_field_projection=False, - **lookup, + **lookup ): """ Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 929fd5260..ad354203a 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -234,7 +234,7 @@ def delete(resource, **lookup): concurrency_check=False, suppress_callbacks=True, original=document, - **lookup, + **lookup ) else: # TODO if the resource schema includes media files, these won't be diff --git a/eve/methods/put.py b/eve/methods/put.py index 7fd4bd55c..603551504 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -140,7 +140,7 @@ def put_internal( concurrency_check, check_auth_value=False, force_auth_field_projection=True, - **lookup, + **lookup ) if not original: if config.UPSERT_ON_PUT: diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 482a50608..638b71568 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -235,7 +235,7 @@ def test_patch_internal(self): self.known_resource, data, concurrency_check=False, - **{"_id": self.item_id}, + **{"_id": self.item_id} ) db_value = self.compare_patch_with_get(test_field, r) self.assertEqual(db_value, test_value) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index a39719a59..43e60990b 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -352,7 +352,7 @@ def test_put_internal(self): self.known_resource, data, concurrency_check=False, - **{"_id": self.item_id}, + **{"_id": self.item_id} ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) @@ -369,7 +369,7 @@ def test_put_internal_skip_validation(self): data, concurrency_check=False, skip_validation=True, - **{"_id": self.item_id}, + **{"_id": self.item_id} ) db_value = self.compare_put_with_get(test_field, r) self.assertEqual(db_value, test_value) From aad14172402a843fc3fcee24af44fc056b102813 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 13:06:47 +0800 Subject: [PATCH 399/821] Fix import error for `DocumentError` --- eve/methods/post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/post.py b/eve/methods/post.py index 23d20ba0d..531ee0aef 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -15,7 +15,7 @@ from flask import current_app as app, abort from eve.utils import config, parse_request, debug_error_message from eve.auth import requires_auth -from eve.validation import DocumentError +from cerberus.validator import DocumentError from eve.methods.common import ( parse, payload, From 864c3c3a5bec0bc51afb005d7365467d97972d0a Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 13:14:54 +0800 Subject: [PATCH 400/821] Fix import error for `DocumentError` --- eve/methods/patch.py | 2 +- eve/methods/put.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index afe48e6be..d3ab5441a 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -16,7 +16,7 @@ from datetime import datetime from eve.utils import config, debug_error_message, parse_request from eve.auth import requires_auth -from eve.validation import DocumentError +from cerberus.validator import DocumentError from eve.methods.common import ( get_document, parse, diff --git a/eve/methods/put.py b/eve/methods/put.py index 603551504..8a522a864 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -32,7 +32,7 @@ ) from eve.methods.post import post_internal from eve.utils import config, debug_error_message, parse_request -from eve.validation import DocumentError +from cerberus.validator import DocumentError from eve.versioning import ( resolve_document_version, insert_versioning_documents, From f4fb5988d458b95cb1756471d98d6680f9ce1e9b Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 23:54:50 +0800 Subject: [PATCH 401/821] Prune the field when its value is {} Previously, the field is pruned when its value starts with $. However, in MongoDB, string starts with $ may means refer to a field in the document. --- eve/methods/get.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 6a4b28c4f..362c87f0b 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -161,7 +161,7 @@ def prune_aggregation_stage(d): Remove the stages whose parameters are not set. For example, we have endpoint with a stage like {'$lookup': {'$userId': '$a', '$name': '$b'}}, $a is provided - but $b is not provided. Then the stage will be pruned as {'$lookup': {'$userId': '$a'}} + but $b is provided as {}. Then the stage will be pruned as {'$lookup': {'$userId': '$a'}} """ for st_key, st_value in d.items(): if isinstance(st_value, dict): @@ -169,9 +169,6 @@ def prune_aggregation_stage(d): if len(st_value.keys()) == 0: # remove the key: value when value is an empty dict del d[st_key] - if st_value[0] == "$": - # remove the key: value when value is not replaced - del d[st_key] response = {} documents = [] From ab99324f8386ff2d9a5b5cbbcfd96f3bbfa3c9c8 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Wed, 5 Dec 2018 23:57:14 +0800 Subject: [PATCH 402/821] Update the test by using {} for missing fields. --- eve/tests/methods/get.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 0aa615fda..3e5495c73 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1510,7 +1510,7 @@ def test_get_aggregation_pruning(self): # look for date = yesterday, which shall return all four results challenge = (date + timedelta(days=-1)).strftime(self.app.config["DATE_FORMAT"]) response, status = self.get( - 'aggregate_test?aggregate={"$date": "%s"}' % challenge + 'aggregate_test?aggregate={"$date": "%s", "$x": {}}' % challenge ) self.assert200(status) @@ -1518,7 +1518,7 @@ def test_get_aggregation_pruning(self): self.assertEqual(len(docs), 4) # look for x = 3, which shall return only one result - response, status = self.get('aggregate_test?aggregate={"x": 3}') + response, status = self.get('aggregate_test?aggregate={"$x": 3, "$date": {}}') self.assert200(status) docs = response["_items"] From 40e772055a420386a7adf8eb4c7917e8fa2def5e Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Thu, 6 Dec 2018 09:29:25 +0800 Subject: [PATCH 403/821] RuntimeError: dictionary changed size during iteration. --- eve/methods/get.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 362c87f0b..210705008 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -163,7 +163,8 @@ def prune_aggregation_stage(d): For example, we have endpoint with a stage like {'$lookup': {'$userId': '$a', '$name': '$b'}}, $a is provided but $b is provided as {}. Then the stage will be pruned as {'$lookup': {'$userId': '$a'}} """ - for st_key, st_value in d.items(): + items = [(st_key, st_value) for st_key, st_value in d.items()] + for (st_key, st_value) in items: if isinstance(st_value, dict): prune_aggregation_stage(st_value) if len(st_value.keys()) == 0: From 69a8438f79ac5784447e31d12002d1070e11b087 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Fri, 7 Dec 2018 16:29:04 +0800 Subject: [PATCH 404/821] Fix a typo in using old pipelines. --- eve/methods/get.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 210705008..bcbd6306d 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -201,7 +201,7 @@ def prune_aggregation_stage(d): req_pipeline_pruned.append(skip) req_pipeline_pruned.append(limit) - getattr(app, "before_aggregation")(resource, req_pipeline) + getattr(app, "before_aggregation")(resource, req_pipeline_pruned) cursor = app.data.aggregate(resource, req_pipeline_pruned, options) From ecc275a0f131527f95b5c88df3c9ff30a7add4f6 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Mon, 10 Dec 2018 15:10:19 +0800 Subject: [PATCH 405/821] Update the documentation for the new feature. --- docs/features.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/features.rst b/docs/features.rst index f1b7a2906..c2d082b3d 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2239,6 +2239,37 @@ to a keyword of your liking, just set ``QUERY_AGGREGATION`` in your settings. You can also set all options natively supported by PyMongo. For more information on aggregation see :ref:`datasource`. +You can pass ``{}`` to fields which you want to ignore. Considering the following pipelines: + +:: + + posts = { + 'datasource': { + 'aggregation': { + 'pipeline': [ + {"$match": { "name": "$name", "time": "$time"}} + {"$unwind": "$tags"}, + {"$group": {"_id": "$tags", "count": {"$sum": 1}}}, + ] + } + } + } + +If performing the following request: + +:: + + $ curl -i http://example.com/posts?aggregate={"$name": {"$regex": "Apple"}, "$time": {}} + +The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline will be executed as ``{"$match": { "name": {"$regex": "Apple"}}}``. And for the following request: + +:: + + $ curl -i http://example.com/posts?aggregate={"$name": {}, "$time": {}} + +The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline will be completely skipped. + +The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. Limitations From ca4365f0aaa205dc0311d410f55db26afcabb25e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Feb 2019 17:38:34 +0100 Subject: [PATCH 406/821] Fix rebase conflict resolution error --- eve/tests/methods/get.py | 51 ++++------------------------------------ 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 3e5495c73..2853612ba 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1465,7 +1465,6 @@ def test_get_aggregation_with_lists(self): docs = response["_items"] self.assertEqual(len(docs), 1) - def test_get_aggregation_pruning(self): date = datetime.utcnow() @@ -1477,6 +1476,9 @@ def test_get_aggregation_pruning(self): {"x": 2, "date": date}, {"x": 3, "date": date}, {"x": 4, "date": date + timedelta(days=-1)}, + ] + ) + self.app.register_resource( "aggregate_test", { @@ -1523,52 +1525,7 @@ def test_get_aggregation_pruning(self): self.assert200(status) docs = response["_items"] self.assertEqual(len(docs), 1) - self.assertEqual(docs[0]['x'], 3) - - def test_get_aggregation_with_lists(self): - _db = self.connection[MONGO_DBNAME] - _db.aggregate_test.insert_many( - [ - {"x": 1, "tags": ["a", "b", "c"]}, - {"x": 2, "tags": ["a"]}, - {"x": 3, "tags": ["a", "b"]}, - {"x": [4], "tags": []}, - ] - ) - - self.app.register_resource( - "aggregate_test", - { - "datasource": { - "aggregation": { - "pipeline": [ - { - "$match": { - "$or": [{"tags": "$match_tags"}, {"x": ["$x"]}] - } - } - ] - } - } - }, - ) - - response, status = self.get('aggregate_test?aggregate={"$match_tags": "a"}') - self.assert200(status) - docs = response["_items"] - self.assertEqual(len(docs), 3) - - response, status = self.get( - 'aggregate_test?aggregate={"$match_tags": ["a", "b"]}' - ) - self.assert200(status) - docs = response["_items"] - self.assertEqual(len(docs), 1) - - response, status = self.get('aggregate_test?aggregate={"$x": 4}') - self.assert200(status) - docs = response["_items"] - self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]["x"], 3) def test_get_aggregation_pagination(self): _db = self.connection[MONGO_DBNAME] From 826ba324e406bb8b304c24832f7828da34ba7255 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Feb 2019 17:41:15 +0100 Subject: [PATCH 407/821] Changelog for #1210 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 803ae9575..b17f1db25 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,7 @@ Fixed Improved ~~~~~~~~ +- Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary inside, and each key-value pair yields the related links for a data relation field (`#1204`_) @@ -21,6 +22,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1209`: https://github.com/pyeve/eve/issues/1209 .. _`#1206`: https://github.com/pyeve/eve/issues/1206 .. _`#1204`: https://github.com/pyeve/eve/pull/1204 .. _`#1194`: https://github.com/pyeve/eve/pull/1194 From 073b7005906e7e6475ef30a16dcb895dc7169993 Mon Sep 17 00:00:00 2001 From: Roller Angel Date: Tue, 8 Jan 2019 14:20:02 -0700 Subject: [PATCH 408/821] Update custom_idfields.rst --- docs/tutorials/custom_idfields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/custom_idfields.rst b/docs/tutorials/custom_idfields.rst index 912a5c6ad..b0f3097fb 100644 --- a/docs/tutorials/custom_idfields.rst +++ b/docs/tutorials/custom_idfields.rst @@ -11,7 +11,7 @@ endpoint will be made available by the framework, and will be used by clients to retrieve and/or edit individual documents. By default, Eve provides this feature seamlessly when ``ID_FIELD`` fields are of ``ObjectId`` type. -However, you might have collections where your unique identifier is not and +However, you might have collections where your unique identifier is not an ``ObjectId``, and you still want individual document endpoints to work properly. Don't worry, it's doable, it only requires a little tinkering. From d02eb34d8f6ffcb7fb47c3f2b47119f59e57a5a1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Feb 2019 17:47:16 +0100 Subject: [PATCH 409/821] Changelog for #1218 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b17f1db25..109f51b24 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Fixed ~~~~~ - Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) +- Documentation typos (`#1218`_) Improved ~~~~~~~~ @@ -22,6 +23,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1218`: https://github.com/pyeve/eve/pull/1218 .. _`#1209`: https://github.com/pyeve/eve/issues/1209 .. _`#1206`: https://github.com/pyeve/eve/issues/1206 .. _`#1204`: https://github.com/pyeve/eve/pull/1204 From 0e336e856a94caee8b368995dc0b5c2b4dccc27c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 4 Feb 2019 17:51:04 +0100 Subject: [PATCH 410/821] Roller Angel --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ad9775b18..af516f712 100644 --- a/AUTHORS +++ b/AUTHORS @@ -144,6 +144,7 @@ Patches and Contributions - Robert Wlodarczyk - Roberto 'Kalamun' Pasini - Rodrigo Rodriguez +- Roller Angel - Roman Gavrilov - Ronan Delacroix - Roy Smith From 498904c53122ad5cbcefc10de8b4add15204b45b Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Wed, 30 Jan 2019 15:14:20 +0100 Subject: [PATCH 411/821] Added VERSION_PARAM to default_params --- eve/methods/get.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eve/methods/get.py b/eve/methods/get.py index bcbd6306d..3406383ac 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -653,6 +653,7 @@ def _other_params(args): config.QUERY_MAX_RESULTS, config.QUERY_EMBEDDED, config.QUERY_PROJECTION, + config.VERSION_PARAM, ] return MultiDict( (key, value) From fe0e6c4460b63dc218a80492d7eb1410fc71395d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 9 Feb 2019 11:44:55 +0100 Subject: [PATCH 412/821] Changelog for #1228 --- .gitignore | 1 + CHANGES.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b3a3ac866..ae7edef8c 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ _build .cache .vscode .pytest_cache +pip-wheel-metadata/ diff --git a/CHANGES.rst b/CHANGES.rst index 109f51b24..bacf5d335 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Version 0.8.2 Fixed ~~~~~ +- HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) - Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) - Documentation typos (`#1218`_) @@ -23,6 +24,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1228`: https://github.com/pyeve/eve/pull/1228 .. _`#1218`: https://github.com/pyeve/eve/pull/1218 .. _`#1209`: https://github.com/pyeve/eve/issues/1209 .. _`#1206`: https://github.com/pyeve/eve/issues/1206 From e800d6e481cb6e72acb2076996215b72818802a4 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sat, 26 Jan 2019 21:51:20 +0100 Subject: [PATCH 413/821] Allow on_fetched event hooks on version=diffs --- eve/methods/get.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 3406383ac..20078b650 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -496,24 +496,23 @@ def getitem_internal(resource, **lookup): ) ) - # callbacks not supported on version diffs because of partial documents - if version != "diffs": - # TODO: callbacks not currently supported with ?version=all - - # notify registered callback functions. Please note that, should - # the functions modify the document, last_modified and etag - # won't be updated to reflect the changes (they always reflect the - # documents state on the database). - if resource_def["versioning"] is True and version == "all": - versions = response - if config.DOMAIN[resource]["hateoas"]: - versions = response[config.ITEMS] - for version_item in versions: - getattr(app, "on_fetched_item")(resource, version_item) - getattr(app, "on_fetched_item_%s" % resource)(version_item) - else: - getattr(app, "on_fetched_item")(resource, response) - getattr(app, "on_fetched_item_%s" % resource)(response) + # callbacks supported on all version methods - even for diffs with partial documents + # partial documents should be handled properly in the callback + # + # notify registered callback functions. Please note that, should + # the functions modify the document, last_modified and etag + # won't be updated to reflect the changes (they always reflect the + # documents state on the database). + if resource_def["versioning"] is True and version in ["all", "diffs"]: + versions = response + if config.DOMAIN[resource]["hateoas"]: + versions = response[config.ITEMS] + for version_item in versions: + getattr(app, "on_fetched_item")(resource, version_item) + getattr(app, "on_fetched_item_%s" % resource)(version_item) + else: + getattr(app, "on_fetched_item")(resource, response) + getattr(app, "on_fetched_item_%s" % resource)(response) return response, last_modified, etag, 200 From 328a5850ba2796927369f4a4019a90b7c11e8772 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sat, 26 Jan 2019 21:51:33 +0100 Subject: [PATCH 414/821] Updated features note on fetch events with versioning --- docs/features.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index c2d082b3d..638431eb8 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1375,9 +1375,11 @@ the items as needed before they are returned to the client. >>> app.on_fetched_item_contacts += before_returning_contact It is important to note that fetch events will work with `Document -Versioning`_ for specific document versions or accessing all document -versions with ``?version=all``, but they *will not* work when accessing diffs -of all versions with ``?version=diffs``. +Versioning`_ for specific document versions like ``?version=5``, accessing all +document versions with ``?version=all``, and accessing diffs of all versions +with ``?version=diffs``. When working with versioning, care should be taken in +the registered callback to handle possible schema differences or partial +documents. Diffs by design is partial documents. Insert Events From 0a4103ebff187597a346926b37733b3f969840a5 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 12:02:08 +0100 Subject: [PATCH 415/821] TestCompleteVersioning success on_fetched diffs --- eve/tests/versioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index a8c8b4802..27c8ba4c4 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -478,7 +478,7 @@ def test_on_fetched_item(self): response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) - self.assertEqual(None, devent.called) + self.assertEqual(2, len(devent.called)) def test_on_fetched_item_contacts(self): """ Verify that on_fetched_item_contacts events are fired for versioned @@ -507,7 +507,7 @@ def test_on_fetched_item_contacts(self): response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) - self.assertEqual(None, devent.called) + self.assertEqual(1, len(devent.called)) # TODO: also test with HATEOS off From caf7a046540937f77e64a180cfc870c2ada1bfb3 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 12:10:41 +0100 Subject: [PATCH 416/821] Added on_fetched_diffs event hook and tests --- eve/methods/get.py | 8 ++++-- eve/tests/versioning.py | 64 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 20078b650..73a190a3a 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -508,8 +508,12 @@ def getitem_internal(resource, **lookup): if config.DOMAIN[resource]["hateoas"]: versions = response[config.ITEMS] for version_item in versions: - getattr(app, "on_fetched_item")(resource, version_item) - getattr(app, "on_fetched_item_%s" % resource)(version_item) + if version == "diffs": + getattr(app, "on_fetched_diffs")(resource, version_item) + getattr(app, "on_fetched_diffs_%s" % resource)(version_item) + else: + getattr(app, "on_fetched_item")(resource, version_item) + getattr(app, "on_fetched_item_%s" % resource)(version_item) else: getattr(app, "on_fetched_item")(resource, response) getattr(app, "on_fetched_item_%s" % resource)(response) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 27c8ba4c4..92777327b 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -478,7 +478,7 @@ def test_on_fetched_item(self): response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) - self.assertEqual(2, len(devent.called)) + self.assertEqual(None, devent.called) def test_on_fetched_item_contacts(self): """ Verify that on_fetched_item_contacts events are fired for versioned @@ -501,6 +501,68 @@ def test_on_fetched_item_contacts(self): self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(1, len(devent.called)) + # check for ?version=diffs requests + devent = DummyEvent(lambda: True) + self.app.on_fetched_item_contacts += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=diffs" + ) + self.assertEqual(None, devent.called) + + # TODO: also test with HATEOS off + + def test_on_fetched_diffs(self): + """ Verify that on_fetched_item events are fired for versioned + requests. + """ + devent = DummyEvent(lambda: True) + self.app.on_fetched_item += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=1" + ) + self.assertEqual(self.known_resource, devent.called[0]) + self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) + self.assertEqual(None, devent.called) + + # check for ?version=all requests + devent = DummyEvent(lambda: True) + self.app.on_fetched_item += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=all" + ) + self.assertEqual(self.known_resource, devent.called[0]) + self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) + self.assertEqual(None, devent.called) + + # check for ?version=diffs requests + devent = DummyEvent(lambda: True) + self.app.on_fetched_item += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=diffs" + ) + self.assertEqual(2, len(devent.called)) + + def test_on_fetched_diffs_contacts(self): + """ Verify that on_fetched_item_contacts events are fired for versioned + requests. + """ + devent = DummyEvent(lambda: True) + self.app.on_fetched_item_contacts += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=1" + ) + self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) + self.assertEqual(None, devent.called) + + # check for ?version=all requests + devent = DummyEvent(lambda: True) + self.app.on_fetched_item_contacts += devent + response, status = self.get( + self.known_resource, item=self.item_id, query="?version=all" + ) + self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) + self.assertEqual(None, devent.called) + # check for ?version=diffs requests devent = DummyEvent(lambda: True) self.app.on_fetched_item_contacts += devent From c9ec64bd7010eb61e7ff214539bee8bfcfc5999e Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 13:33:38 +0100 Subject: [PATCH 417/821] Added test_on_fetched_diffs and test_on_fetched_diffs_contacts --- eve/tests/versioning.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 92777327b..4f550f31b 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -512,63 +512,59 @@ def test_on_fetched_item_contacts(self): # TODO: also test with HATEOS off def test_on_fetched_diffs(self): - """ Verify that on_fetched_item events are fired for versioned - requests. + """ Verify that on_fetched_item events are fired for + version=diffs requests. """ devent = DummyEvent(lambda: True) - self.app.on_fetched_item += devent + self.app.on_fetched_diffs += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=1" ) - self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) self.assertEqual(None, devent.called) # check for ?version=all requests devent = DummyEvent(lambda: True) - self.app.on_fetched_item += devent + self.app.on_fetched_diffs += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=all" ) - self.assertEqual(self.known_resource, devent.called[0]) - self.assertEqual(self.item_id, str(devent.called[1][self.id_field])) self.assertEqual(None, devent.called) # check for ?version=diffs requests devent = DummyEvent(lambda: True) - self.app.on_fetched_item += devent + self.app.on_fetched_diffs += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) + self.assertEqual(self.known_resource, devent.called[0]) self.assertEqual(2, len(devent.called)) def test_on_fetched_diffs_contacts(self): - """ Verify that on_fetched_item_contacts events are fired for versioned - requests. + """ Verify that on_fetched_diffs_contacts events are fired for + version=diffs requests. """ devent = DummyEvent(lambda: True) - self.app.on_fetched_item_contacts += devent + self.app.on_fetched_diffs_contacts += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=1" ) - self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(None, devent.called) # check for ?version=all requests devent = DummyEvent(lambda: True) - self.app.on_fetched_item_contacts += devent + self.app.on_fetched_diffs_contacts += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=all" ) - self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(None, devent.called) # check for ?version=diffs requests devent = DummyEvent(lambda: True) - self.app.on_fetched_item_contacts += devent + self.app.on_fetched_diffs_contacts += devent response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) + self.assertEqual(self.known_resource, devent.called[0]) self.assertEqual(1, len(devent.called)) # TODO: also test with HATEOS off From 2e41102914f390d8e9dacd9b4588c96b6aa424c4 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 15:09:36 +0100 Subject: [PATCH 418/821] Fetch Events, on_fetched_diffs added --- docs/features.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 638431eb8..459359369 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1270,6 +1270,12 @@ Let's see an overview of what events are available: | | | +--------------------------------------------------+ | | | || ``on_fetched_item_`` | | | | || ``def event(response)`` | +| +--------+------+--------------------------------------------------+ +| |Diffs |After || ``on_fetched_diffs`` | +| | | || ``def event(resource_name, response)`` | +| | | +--------------------------------------------------+ +| | | || ``on_fetched_diffs_`` | +| | | || ``def event(response)`` | +-------+--------+------+--------------------------------------------------+ |Insert |Items |Before|| ``on_insert`` | | | | || ``def event(resource_name, items)`` | @@ -1349,6 +1355,8 @@ These are the fetch events with their method signature: - ``on_fetched_resource_(response)`` - ``on_fetched_item(resource_name, response)`` - ``on_fetched_item_(response)`` +- ``on_fetched_diffs(resource_name, response)`` +- ``on_fetched_diffs_(response)`` They are raised when items have just been read from the database and are about to be sent to the client. Registered callback functions can manipulate @@ -1374,12 +1382,12 @@ the items as needed before they are returned to the client. >>> app.on_fetched_item += before_returning_item >>> app.on_fetched_item_contacts += before_returning_contact -It is important to note that fetch events will work with `Document -Versioning`_ for specific document versions like ``?version=5``, accessing all -document versions with ``?version=all``, and accessing diffs of all versions -with ``?version=diffs``. When working with versioning, care should be taken in -the registered callback to handle possible schema differences or partial -documents. Diffs by design is partial documents. +It is important to note that item fetch events will work with `Document +Versioning`_ for specific document versions like ``?version=5`` and all +document versions with ``?version=all``. Accessing diffs of all versions +with ``?version=diffs`` will only work with the diffs fetch events. Note +that diffs returns partial documents which should be handled in the +callback. Insert Events From 727c819a4a8a54f51ea7a4cb4e897abe91dc92f9 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 15:51:42 +0100 Subject: [PATCH 419/821] Fixed error in test_getitem_version_diffs --- eve/tests/versioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 4f550f31b..33df5483a 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -564,7 +564,7 @@ def test_on_fetched_diffs_contacts(self): response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) - self.assertEqual(self.known_resource, devent.called[0]) + self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) self.assertEqual(1, len(devent.called)) # TODO: also test with HATEOS off From c3a7b61dadda0df08a73871d86f3461cdb0d5938 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Mon, 28 Jan 2019 17:53:51 +0100 Subject: [PATCH 420/821] Moved diffs out of iteration --- eve/methods/get.py | 12 +++++++----- eve/tests/versioning.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 73a190a3a..b14c851e9 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -129,6 +129,7 @@ def _perform_aggregation(resource, pipeline, options): """ .. versionadded:: 0.7 """ + # TODO move most of this down to the Mongo layer? # TODO experiment with cursor.batch_size as alternative pagination @@ -507,11 +508,12 @@ def getitem_internal(resource, **lookup): versions = response if config.DOMAIN[resource]["hateoas"]: versions = response[config.ITEMS] - for version_item in versions: - if version == "diffs": - getattr(app, "on_fetched_diffs")(resource, version_item) - getattr(app, "on_fetched_diffs_%s" % resource)(version_item) - else: + + if version == "diffs": + getattr(app, "on_fetched_diffs")(resource, versions) + getattr(app, "on_fetched_diffs_%s" % resource)(versions) + else: + for version_item in versions: getattr(app, "on_fetched_item")(resource, version_item) getattr(app, "on_fetched_item_%s" % resource)(version_item) else: diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 33df5483a..eb805de2d 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -564,7 +564,8 @@ def test_on_fetched_diffs_contacts(self): response, status = self.get( self.known_resource, item=self.item_id, query="?version=diffs" ) - self.assertEqual(self.item_id, str(devent.called[0][self.id_field])) + # Verify first document has id_field + self.assertEqual(self.item_id, str(devent.called[0][0][self.id_field])) self.assertEqual(1, len(devent.called)) # TODO: also test with HATEOS off From 7b04229b697e3794acca9ad76473a20242b0c750 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 9 Feb 2019 11:53:37 +0100 Subject: [PATCH 421/821] Changelog for #1224 --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bacf5d335..f0e37e58d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,10 @@ Here you can see the full list of changes between each Eve release. Version 0.8.2 ------------- +New +~~~ +- ``on_fetched_diffs`` event hooks (`#1224`_) + Fixed ~~~~~ - HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) @@ -24,6 +28,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1224`: https://github.com/pyeve/eve/pull/1224 .. _`#1228`: https://github.com/pyeve/eve/pull/1228 .. _`#1218`: https://github.com/pyeve/eve/pull/1218 .. _`#1209`: https://github.com/pyeve/eve/issues/1209 From 413d539a3779d850ba90929b4954373cd72fb4c6 Mon Sep 17 00:00:00 2001 From: Einar Huseby Date: Sun, 27 Jan 2019 16:48:19 +0100 Subject: [PATCH 422/821] Fixes #1069 following data_relation.field --- eve/methods/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 91c678de0..47615391f 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -984,7 +984,8 @@ def generate_query_and_sorting_criteria(data_relation, references): id_field_name = ( "_id" if isinstance(reference, DBRef) - else config.DOMAIN[subresource]["id_field"] + else data_relation.get("field", False) + or config.DOMAIN[subresource]["id_field"] ) id_field_value = reference.id if isinstance(reference, DBRef) else reference query["$or"].append({id_field_name: id_field_value}) From f8cfdbe7dad3486f64dfae1089c0a3e9e5ced53a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 Feb 2019 10:10:06 +0100 Subject: [PATCH 423/821] Changelog for #1225 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f0e37e58d..6cebf5161 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ New Fixed ~~~~~ +- Embedding only does not follow ``data_relation.field`` (`#1069`_) - HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) - Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) @@ -28,6 +29,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1069`: https://github.com/pyeve/eve/issues/1069 .. _`#1224`: https://github.com/pyeve/eve/pull/1224 .. _`#1228`: https://github.com/pyeve/eve/pull/1228 .. _`#1218`: https://github.com/pyeve/eve/pull/1218 From 84d315b5b8d9707ab994f57d100a145e440946bb Mon Sep 17 00:00:00 2001 From: Chuck Turco Date: Sun, 3 Mar 2019 16:43:43 -0600 Subject: [PATCH 424/821] Atomic Concurrency Checks for pyeve/eve#1231 --- eve/io/mongo/mongo.py | 13 +++- eve/methods/patch.py | 7 +- eve/tests/methods/patch_atomic_concurrency.py | 73 +++++++++++++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 eve/tests/methods/patch_atomic_concurrency.py diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index fa308bbea..f917a3fd5 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -469,9 +469,18 @@ def _change_request(self, resource, id_, changes, original, replace=False): coll = self.get_collection_with_write_concern(datasource, resource) try: - coll.replace_one(filter_, changes) if replace else coll.update_one( - filter_, changes + result = ( + coll.replace_one(filter_, changes) + if replace + else coll.update_one(filter_, changes) ) + if ( + config.ETAG in original + and result + and result.acknowledged + and result.modified_count == 0 + ): + raise self.OriginalChangedError() except pymongo.errors.DuplicateKeyError as e: abort( 400, diff --git a/eve/methods/patch.py b/eve/methods/patch.py index d3ab5441a..e6270f687 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -215,8 +215,11 @@ def patch_internal( resolve_document_etag(updated, resource) # now storing the (updated) ETAG with every document (#453) updates[config.ETAG] = updated[config.ETAG] - - app.data.update(resource, object_id, updates, original) + try: + app.data.update(resource, object_id, updates, original) + except app.data.OriginalChangedError: + if concurrency_check: + abort(412, description="Client and server etags don't match") # update oplog if needed oplog_push(resource, updates, "PATCH", object_id) diff --git a/eve/tests/methods/patch_atomic_concurrency.py b/eve/tests/methods/patch_atomic_concurrency.py new file mode 100644 index 000000000..e392523b6 --- /dev/null +++ b/eve/tests/methods/patch_atomic_concurrency.py @@ -0,0 +1,73 @@ +import simplejson as json +import sys +import eve.methods.common +from eve.tests import TestBase +from eve.utils import config + +""" +Atomic Concurrency Checks + +Prior to commit 54fd697 from 2016-November, ETags would be verified +twice during a patch. One ETag check would be non-atomic by Eve, +then again atomically by MongoDB during app.data.update(filter). +The atomic ETag check was removed during issue #920 in 54fd697 + +When running Eve in a scale-out environment (multiple processes), +concurrent simultaneous updates are sometimes allowed, because +the Python-only ETag check is not atomic. + +There is a critical section in patch_internal() between get_document() +and app.data.update() where a competing Eve process can change the +document and ETag. + +This test simulates another process changing data & ETag during +the critical section. The test patches get_document() to return an +intentionally wrong ETag. +""" + + +def get_document_simulate_concurrent_update(*args, **kwargs): + """ + Hostile version of get_document + + This simluates another process updating MongoDB (and ETag) in + eve.methods.patch.patch_internal() during the critical area + between get_document() and app.data.update() + """ + document = eve.methods.common.get_document(*args, **kwargs) + document[config.ETAG] = "unexpected change!" + return document + + +class TestPatchAtomicConcurrent(TestBase): + def setUp(self): + """ + Patch eve.methods.patch.get_document with a hostile version + that simulates simultaneous updates + """ + self.original_get_document = sys.modules["eve.methods.patch"].get_document + sys.modules[ + "eve.methods.patch" + ].get_document = get_document_simulate_concurrent_update + return super(TestPatchAtomicConcurrent, self).setUp() + + def test_etag_changed_after_get_document(self): + """ + Try to update a document after the ETag was adjusted + outside this process + """ + changes = {"ref": "1234567890123456789054321"} + _r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + ) + self.assertEqual(status, 412) + + def tearDown(self): + """ Remove patch of eve.methods.patch.get_document """ + sys.modules["eve.methods.patch"].get_document = self.original_get_document + return super(TestPatchAtomicConcurrent, self).tearDown() + + def patch(self, url, data, headers=[]): + headers.append(("Content-Type", "application/json")) + r = self.test_client.patch(url, data=json.dumps(data), headers=headers) + return self.parse_response(r) From 8f3a3b58dd163ef59b639147d9d6ff690e505f2c Mon Sep 17 00:00:00 2001 From: Chuck Turco Date: Sun, 3 Mar 2019 17:50:34 -0600 Subject: [PATCH 425/821] correct whitespace issue --- eve/tests/methods/patch_atomic_concurrency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/methods/patch_atomic_concurrency.py b/eve/tests/methods/patch_atomic_concurrency.py index e392523b6..2b3fac82e 100644 --- a/eve/tests/methods/patch_atomic_concurrency.py +++ b/eve/tests/methods/patch_atomic_concurrency.py @@ -27,7 +27,7 @@ def get_document_simulate_concurrent_update(*args, **kwargs): - """ + """ Hostile version of get_document This simluates another process updating MongoDB (and ETag) in From 355c96751bb711df46f192c2941767055e3f04f7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 14 Mar 2019 11:22:25 +0100 Subject: [PATCH 426/821] Changelog for #1232 --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6cebf5161..884c25193 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,9 @@ New Fixed ~~~~~ +- Multiple concurrent patches to the same record, from different processes, + should result in at least one patch failing with a 412 error (Precondition + Failed) (`#1231`_) - Embedding only does not follow ``data_relation.field`` (`#1069`_) - HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) - Do not alter ETag when performing an oplog_push (`#1206`_) @@ -29,6 +32,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1231`: https://github.com/pyeve/eve/issues/1231 .. _`#1069`: https://github.com/pyeve/eve/issues/1069 .. _`#1224`: https://github.com/pyeve/eve/pull/1224 .. _`#1228`: https://github.com/pyeve/eve/pull/1228 From 05308ce1b883c1a122ca3ff3498b6bb9ca2f5bad Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 14 Mar 2019 11:22:43 +0100 Subject: [PATCH 427/821] Chuck Turco --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index af516f712..ea6d82999 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,7 @@ Patches and Contributions - Christian Henke - Christoph Witzany - Christopher Larsen +- Chuck Turco - Conrad Burchert - Cyprien Pannier - Cyril Bonnard From ea00ffab2da37e8559daa5a35d2bf93e481100ca Mon Sep 17 00:00:00 2001 From: Phone Myint Kyaw Date: Thu, 7 Feb 2019 14:41:19 +0630 Subject: [PATCH 428/821] Update 2019 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index afe9010b7..dcfc0027f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 by Nicola Iarocci and contributors. See AUTHORS +Copyright (c) 2019 by Nicola Iarocci and contributors. See AUTHORS for more details. Some rights reserved. From 4bfb1b978f446a0e236bfbbc852b86a1891188b7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 14 Mar 2019 11:28:25 +0100 Subject: [PATCH 429/821] Phone Myint Kyaw --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ea6d82999..f5aaac6c8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -138,6 +138,7 @@ Patches and Contributions - Paul Doucet - Peter Darrow - Petr Jašek +- Phone Myint Kyaw - Prayag Verma - Qiang Zhang - Ralph Smith From a14424619fba3337298a6371d958d98ad95e90cf Mon Sep 17 00:00:00 2001 From: Aayush Sarva Date: Fri, 15 Mar 2019 21:56:41 +0530 Subject: [PATCH 430/821] Add JSON response for rate limit exceeded --- eve/default_settings.py | 2 +- eve/methods/common.py | 6 ++-- eve/tests/__init__.py | 3 ++ eve/tests/auth.py | 18 +++++------ eve/tests/config.py | 24 +++++++-------- eve/tests/endpoints.py | 12 ++++---- eve/tests/methods/get.py | 2 +- eve/tests/methods/patch.py | 6 ++-- eve/tests/methods/post.py | 56 +++++++++++++++++----------------- eve/tests/methods/put.py | 6 ++-- eve/tests/methods/ratelimit.py | 3 +- eve/tests/renders.py | 10 +++--- eve/tests/versioning.py | 14 ++++----- 13 files changed, 81 insertions(+), 81 deletions(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index 7d092fb3c..38dc36e2f 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -128,7 +128,7 @@ # codes for which we want to return a standard response which includes # a JSON body with the status, code, and description. -STANDARD_ERRORS = [400, 401, 404, 405, 406, 409, 410, 412, 422, 428] +STANDARD_ERRORS = [400, 401, 404, 405, 406, 409, 410, 412, 422, 428, 429] # field returned on GET requests so we know if we have the latest copy even if # we access a specific version diff --git a/eve/methods/common.py b/eve/methods/common.py index 47615391f..4f78c31b4 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -20,7 +20,7 @@ from bson.dbref import DBRef from bson.errors import InvalidId from cerberus import schema_registry, rules_set_registry -from flask import Response, abort, current_app as app, g, request +from flask import abort, current_app as app, g, request from werkzeug.datastructures import MultiDict, CombinedMultiDict from eve.utils import ( @@ -309,7 +309,7 @@ def rate_limited(*args, **kwargs): ) rlimit = RateLimit(key, limit, period, True) if rlimit.over_limit: - return Response("Rate limit exceeded", 429) + abort(429, "Rate limit exceeded") # store the rate limit for further processing by # send_response g._rate_limit = rlimit @@ -1159,7 +1159,7 @@ def marshal_write_response(document, resource): auth_field = resource_def.get("auth_field") if auth_field and auth_field not in resource_def["schema"]: try: - del (document[auth_field]) + del document[auth_field] except: # 'auth_field' value has not been set by the auth class. pass diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index badc531fd..834b63b63 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -353,6 +353,9 @@ def assert412(self, status): def assert428(self, status): self.assertEqual(status, 428) + def assert429(self, status): + self.assertEqual(status, 429) + def assert500(self, status): self.assertEqual(status, 500) diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 8600b7226..651df8ffe 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -202,13 +202,13 @@ def test_public_methods_resource(self): self.app.config["PUBLIC_METHODS"] = ["GET"] domain = self.app.config["DOMAIN"] for resource, settings in domain.items(): - del (settings["public_methods"]) + del settings["public_methods"] self.app.set_defaults() - del (domain["peopleinvoices"]) - del (domain["peoplerequiredinvoices"]) - del (domain["peoplesearches"]) - del (domain["internal_transactions"]) - del (domain["child_products"]) + del domain["peopleinvoices"] + del domain["peoplerequiredinvoices"] + del domain["peoplesearches"] + del domain["internal_transactions"] + del domain["child_products"] for resource in domain: url = self.app.config["URLS"][resource] r = self.test_client.get(url) @@ -223,7 +223,7 @@ def test_public_methods_but_locked_resource(self): self.app.config["PUBLIC_METHODS"] = ["GET"] domain = self.app.config["DOMAIN"] for _, settings in domain.items(): - del (settings["public_methods"]) + del settings["public_methods"] self.app.set_defaults() domain[self.known_resource]["public_methods"] = [] r = self.test_client.get(self.known_resource_url) @@ -233,7 +233,7 @@ def test_public_methods_but_locked_item(self): self.app.config["PUBLIC_ITEM_METHODS"] = ["GET"] domain = self.app.config["DOMAIN"] for _, settings in domain.items(): - del (settings["public_item_methods"]) + del settings["public_item_methods"] self.app.set_defaults() domain[self.known_resource]["public_item_methods"] = [] r = self.test_client.get(self.item_id_url) @@ -242,7 +242,7 @@ def test_public_methods_but_locked_item(self): def test_public_methods_item(self): self.app.config["PUBLIC_ITEM_METHODS"] = ["GET"] for _, settings in self.app.config["DOMAIN"].items(): - del (settings["public_item_methods"]) + del settings["public_item_methods"] self.app.set_defaults() # we're happy with testing just one client endpoint, but for sake of # completeness we shold probably test item endpoints for every resource diff --git a/eve/tests/config.py b/eve/tests/config.py index a002b7e15..85d4e6f04 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -84,7 +84,7 @@ def test_default_settings(self): self.assertEqual(self.app.config["SHOW_DELETED_PARAM"], "show_deleted") self.assertEqual( self.app.config["STANDARD_ERRORS"], - [400, 401, 404, 405, 406, 409, 410, 412, 422, 428], + [400, 401, 404, 405, 406, 409, 410, 412, 422, 428, 429], ) self.assertEqual(self.app.config["UPSERT_ON_PUT"], True) self.assertEqual( @@ -173,18 +173,18 @@ def assertUnallowedField(self, field, field_type="datetime"): def test_validate_schema(self): # lack of 'collection' key for 'data_collection' rule schema = self.domain["invoices"]["schema"] - del (schema["person"]["data_relation"]["resource"]) + del schema["person"]["data_relation"]["resource"] self.assertValidateSchemaFailure("invoices", schema, "resource") def test_validate_invalid_field_names(self): schema = self.domain["invoices"]["schema"] schema["te$t"] = {"type": "string"} self.assertValidateSchemaFailure("invoices", schema, "te$t") - del (schema["te$t"]) + del schema["te$t"] schema["te.t"] = {"type": "string"} self.assertValidateSchemaFailure("invoices", schema, "te.t") - del (schema["te.t"]) + del schema["te.t"] schema["test_a_dict_schema"] = { "type": "dict", @@ -356,7 +356,7 @@ def test_url_helpers(self): self.assertNotEqual(self.app.config.get("SOURCES"), None) self.assertEqual(type(self.app.config["SOURCES"]), dict) - del (self.domain["internal_transactions"]) + del self.domain["internal_transactions"] for resource, settings in self.domain.items(): self.assertEqual( settings["datasource"], self.app.config["SOURCES"][resource] @@ -374,11 +374,11 @@ def test_pretty_resource_urls(self): def test_url_rules(self): map_adapter = self.app.url_map.bind("") - del (self.domain["peopleinvoices"]) - del (self.domain["peoplerequiredinvoices"]) - del (self.domain["peoplesearches"]) - del (self.domain["internal_transactions"]) - del (self.domain["child_products"]) + del self.domain["peopleinvoices"] + del self.domain["peoplerequiredinvoices"] + del self.domain["peoplesearches"] + del self.domain["internal_transactions"] + del self.domain["child_products"] for _, settings in self.domain.items(): for method in settings["resource_methods"]: self.assertTrue(map_adapter.test("/%s/" % settings["url"], method)) @@ -423,7 +423,7 @@ def test_oplog_config(self): self.app.config["OPLOG_ENDPOINT"] = "oplog" self.app._init_oplog() self.assertOplog("oplog", "oplog") - del (self.domain["oplog"]) + del self.domain["oplog"] # OPLOG can be also with a custom name (which will be used # as the collection/table name on the db) @@ -431,7 +431,7 @@ def test_oplog_config(self): self.app.config["OPLOG_NAME"] = oplog self.app._init_oplog() self.assertOplog(oplog, "oplog") - del (self.domain[oplog]) + del self.domain[oplog] # oplog can be defined as a regular API endpoint, with a couple caveats self.domain["oplog"] = { diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 9fe22fc32..bdbaf5e0f 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -137,11 +137,11 @@ def test_homepage(self): self.assertEqual(r.status_code, 200) def test_resource_endpoint(self): - del (self.domain["peopleinvoices"]) - del (self.domain["peoplerequiredinvoices"]) - del (self.domain["peoplesearches"]) - del (self.domain["internal_transactions"]) - del (self.domain["child_products"]) + del self.domain["peopleinvoices"] + del self.domain["peoplerequiredinvoices"] + del self.domain["peoplesearches"] + del self.domain["internal_transactions"] + del self.domain["child_products"] for settings in self.domain.values(): r = self.test_client.get("/%s/" % settings["url"]) self.assert200(r.status_code) @@ -292,7 +292,7 @@ def on_generic_inserted(self, resource, docs): def test_internal_endpoint(self): self.app.on_inserted -= self.on_generic_inserted self.app.on_inserted += self.on_generic_inserted - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "rows" test_value = [{"sku": "AT1234", "price": 99}, {"sku": "XF9876", "price": 9999}] data = {test_field: test_value} diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2853612ba..6a2a54a50 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1596,7 +1596,7 @@ def test_get_aggregation_pagination(self): self.assertEqual(len(items), num) def test_get_query_bitwise_query_operators(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] response, status = self.delete(self.known_resource_url) self.assert204(status) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 638b71568..5d90bbab5 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -333,7 +333,7 @@ def test_patch_x_www_form_urlencoded(self): self.assertTrue("OK" in r[STATUS]) def test_patch_x_www_form_urlencoded_number_serialization(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] field = "anumber" test_value = 3.5 changes = {field: test_value} @@ -462,7 +462,7 @@ def test_patch_bandwidth_saver(self): def test_patch_readonly_field_with_previous_document(self): schema = self.domain["contacts"]["schema"] - del (schema["ref"]["required"]) + del schema["ref"]["required"] # disable read-only on the field so we can store a value which is # also different form its default value. @@ -617,7 +617,7 @@ def test_patch_dependent_field_on_origin_document(self): """ # this will fail as dependent field is missing even in the # document we are trying to update. - del (self.domain["contacts"]["schema"]["dependency_field1"]["default"]) + del self.domain["contacts"]["schema"]["dependency_field1"]["default"] changes = {"dependency_field2": "value"} r, status = self.patch( self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 97e30e269..b1e621d9b 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -74,63 +74,63 @@ def test_post_duplicate_key(self): self.assertEqual(status, 409) def test_post_integer(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "prog" test_value = 1 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_list_as_array(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "role" test_value = ["vendor", "client"] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_rows(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "rows" test_value = [{"sku": "AT1234", "price": 99}, {"sku": "XF9876", "price": 9999}] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_list(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "alist" test_value = ["a_string", 99] data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_integer_zero(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "aninteger" test_value = 0 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_float_zero(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "afloat" test_value = 0.0 data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_dict(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "location" test_value = {"address": "an address", "city": "a city"} data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_datetime(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "born" test_value = "Tue, 06 Nov 2012 10:33:31 GMT" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_objectid(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "tid" test_value = "50656e4538345b39dd0414f0" data = {test_field: test_value} @@ -138,7 +138,7 @@ def test_post_objectid(self): def test_post_null_objectid(self): # verify that #341 is fixed. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "tid" test_value = None data = {test_field: test_value} @@ -234,7 +234,7 @@ def test_post_x_www_form_urlencoded(self): self.assertPostResponse(r) def test_post_x_www_form_urlencoded_number_serialization(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "anumber" test_value = 34 data = {test_field: test_value} @@ -395,7 +395,7 @@ def test_post_referential_integrity_list(self): self.assertPostResponse(r) def test_post_allow_unknown(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = {"unknown": "unknown"} r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) @@ -497,7 +497,7 @@ def test_post_with_get_override(self): def test_post_list_of_objectid(self): objectid = "50656e4538345b39dd0414f0" - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = {"id_list": ["%s" % objectid]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) @@ -510,7 +510,7 @@ def test_post_list_of_objectid(self): def test_post_nested_dict_objectid(self): objectid = "50656e4538345b39dd0414f0" - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = {"id_list_of_dict": [{"id": "%s" % objectid}]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) @@ -521,14 +521,14 @@ def test_post_nested_dict_objectid(self): self.assertTrue("%s" % objectid in r["_items"][0]["id_list_of_dict"][0]["id"]) def test_post_valueschema_with_objectid(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = {"dict_valueschema": {"id": {"challenge": "50656e4538345b39dd0414f0"}}} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) def test_post_list_fixed_len(self): objectid = "50656e4538345b39dd0414f0" - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = {"id_list_fixed_len": ["%s" % objectid]} r, status = self.post(self.known_resource_url, data=data) self.assert201(status) @@ -664,14 +664,14 @@ def test_post_alternative_payload(self): def test_post_dependency_fields_with_default(self): # test that default values are resolved before validation. See #353. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "dependency_field2" test_value = "a value" data = {test_field: test_value} self.assertPostItem(data, test_field, test_value) def test_post_dependency_required_fields(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] schema = self.domain["contacts"]["schema"] schema["dependency_field3"]["required"] = True @@ -694,7 +694,7 @@ def test_post_dependency_required_fields(self): def test_post_dependency_fields_with_values(self): # test that dependencies values are validated correctly. See #547. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] schema = { "field1": {"required": False, "default": "one"}, @@ -726,7 +726,7 @@ def test_post_dependency_fields_with_values(self): def test_post_dependency_fields_with_subdocuments(self): # test that dependencies with sub-document fields are properly # validated. See #706. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] schema = { "field1": {"type": "dict", "schema": {"address": {"type": "string"}}}, @@ -754,7 +754,7 @@ def test_post_dependency_fields_with_subdocuments(self): def test_post_readonly_field_with_default(self): # test that a read only field with a 'default' setting is correctly # validated now that we resolve field values before validation. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "read_only_field" # thou shalt not pass. test_value = "a random value" @@ -771,7 +771,7 @@ def test_post_readonly_field_with_default(self): def test_post_readonly_in_dict(self): # Test that a post with a readonly field inside a dict is properly # validated (even if it has a defult value) - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "dict_with_read_only" test_value = {"read_only_in_dict": "default"} data = {test_field: test_value} @@ -780,7 +780,7 @@ def test_post_readonly_in_dict(self): def test_post_valueschema_dict(self): """ make sure Cerberus#48 is fixed """ - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] r, status = self.post( self.known_resource_url, data={"valueschema_dict": {"k1": "1"}} ) @@ -795,7 +795,7 @@ def test_post_valueschema_dict(self): self.assert201(status) def test_post_keyschema_dict(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] r, status = self.post( self.known_resource_url, data={"keyschema_dict": {"aaa": 1}} @@ -835,7 +835,7 @@ def test_post_internal_skip_validation(self): self.assert201(status) def test_post_nested(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] data = { "location.city": "a nested city", "location.address": "a nested address", @@ -849,7 +849,7 @@ def test_post_nested(self): self.assertEqual(values["address"], "a nested address") def test_post_error_as_list(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] self.app.config["VALIDATION_ERROR_AS_LIST"] = True data = {"unknown_field": "a value"} r, status = self.post(self.known_resource_url, data=data) @@ -905,7 +905,7 @@ def test_post_custom_json_content_type(self): def test_post_updating_a_document_with_nullable_data_relation_does_not_fail(self): # See #1159. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] employee = { "employer": { @@ -924,7 +924,7 @@ def test_post_updating_a_document_with_nullable_data_relation_does_not_fail(self r, s = self.post("employee", data=data) self.assert422(s) - del (employee["employer"]["nullable"]) + del employee["employer"]["nullable"] r, s = self.post("employee", data=data) self.assert422(s) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index 43e60990b..b7c5f6497 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -106,7 +106,7 @@ def test_put_x_www_form_urlencoded(self): self.assertTrue("OK" in r[STATUS]) def test_put_x_www_form_urlencoded_number_serialization(self): - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] field = "anumber" test_value = 41 changes = {field: test_value} @@ -313,7 +313,7 @@ def test_put_bandwidth_saver(self): def test_put_dependency_fields_with_default(self): # Test that if a dependency is missing but has a default value then the # field is still accepted. See #353. - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] field = "dependency_field2" test_value = "a value" changes = {field: test_value} @@ -323,7 +323,7 @@ def test_put_dependency_fields_with_default(self): def test_put_dependency_fields_with_wrong_value(self): # Test that if a dependency is not met, the put is refused - del (self.domain["contacts"]["schema"]["ref"]["required"]) + del self.domain["contacts"]["schema"]["ref"]["required"] r, status = self.put( self.item_id_url, data={"dependency_field3": "value"}, diff --git a/eve/tests/methods/ratelimit.py b/eve/tests/methods/ratelimit.py index dfabedeca..79dbf5b0d 100644 --- a/eve/tests/methods/ratelimit.py +++ b/eve/tests/methods/ratelimit.py @@ -51,8 +51,7 @@ def get_ratelimit(self, url): if t1 != t2: time.sleep(1) self.assertRateLimit(r1) - self.assertEqual(r2.status_code, 429) - self.assertTrue(b"Rate limit exceeded" in r2.get_data()) + self.assert429(r2.status_code) time.sleep(1) self.assertRateLimit(self.test_client.get(url)) diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 18056ef17..0dda58333 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -338,11 +338,11 @@ def test_CORS_OPTIONS_resources(self): self.app.config["URL_PREFIX"], self.app.config["API_VERSION"] ) - del (self.domain["peopleinvoices"]) - del (self.domain["peoplerequiredinvoices"]) - del (self.domain["peoplesearches"]) - del (self.domain["internal_transactions"]) - del (self.domain["child_products"]) + del self.domain["peopleinvoices"] + del self.domain["peoplerequiredinvoices"] + del self.domain["peoplesearches"] + del self.domain["internal_transactions"] + del self.domain["child_products"] for _, settings in self.app.config["DOMAIN"].items(): # resource endpoint url = "%s/%s/" % (prefix, settings["url"]) diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index eb805de2d..115c90b36 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -30,14 +30,12 @@ def tearDown(self): self.connection.close() def enableVersioning(self, partial=False): - del (self.domain["contacts"]["schema"]["title"]["default"]) - del (self.domain["contacts"]["schema"]["dependency_field1"]["default"]) - del (self.domain["contacts"]["schema"]["read_only_field"]["default"]) - del ( - self.domain["contacts"]["schema"]["dict_with_read_only"]["schema"][ - "read_only_in_dict" - ]["default"] - ) + del self.domain["contacts"]["schema"]["title"]["default"] + del self.domain["contacts"]["schema"]["dependency_field1"]["default"] + del self.domain["contacts"]["schema"]["read_only_field"]["default"] + del self.domain["contacts"]["schema"]["dict_with_read_only"]["schema"][ + "read_only_in_dict" + ]["default"] if partial is True: contact_schema = self.domain["contacts"]["schema"] contact_schema[self.unversioned_field]["versioned"] = False From 7b04c904455db70d8ce42891e5871bff82d504e0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Mar 2019 07:39:46 -0700 Subject: [PATCH 431/821] Changelog for #1236 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 884c25193..4e17b480a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ New Fixed ~~~~~ +- Expecting JSON response for rate limit exceeded scenario (`#1227`_) - Multiple concurrent patches to the same record, from different processes, should result in at least one patch failing with a 412 error (Precondition Failed) (`#1231`_) @@ -32,6 +33,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1227`: https://github.com/pyeve/eve/issues/1227 .. _`#1231`: https://github.com/pyeve/eve/issues/1231 .. _`#1069`: https://github.com/pyeve/eve/issues/1069 .. _`#1224`: https://github.com/pyeve/eve/pull/1224 From c897ec9124d8da6514a2697a51f3f88b966fc35f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Mar 2019 07:40:57 -0700 Subject: [PATCH 432/821] Aayush Sarva --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index f5aaac6c8..f536b0c13 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,8 @@ Development Lead Patches and Contributions ````````````````````````` + +- Aayush Sarva - Alexander Dietmüller - Alexander Hendorf - Amedeo Bussi From bf53a7974f42ace2160639e38485d1fbb917197e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 20 Mar 2019 14:42:29 -0700 Subject: [PATCH 433/821] Pin werkzeug to 0.14.1 With v0.15 the test suite breaks our auth tests. Culprit appears to be: https://github.com/pallets/werkzeug/pull/1325 Pinning the dependency until I have time to look into it. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6e5d85341..b84e52434 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ "flask>=1.0", "pymongo>=3.5", "simplejson>=3.3.0,<4.0", + "werkzeug<=0.14.1", ] EXTRAS_REQUIRE = { From f6262f4ca5250532e73f1a7939b0cab40ab55d5f Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Fri, 22 Mar 2019 21:42:02 +0000 Subject: [PATCH 434/821] docs, not valid python can't assign to literal --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 61786cd64..34ae85433 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1161,7 +1161,7 @@ streams. :: # 'people' schema definition - 'schema'= { + schema = { 'firstname': { 'type': 'string', 'minlength': 1, From 8552d2918267a31e626c122ecc83e0ee83d5e553 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 23 Mar 2019 16:40:57 +0100 Subject: [PATCH 435/821] Pedro Rodrigues --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index f536b0c13..68ff7f908 100644 --- a/AUTHORS +++ b/AUTHORS @@ -138,6 +138,7 @@ Patches and Contributions - Patrick Decat - Pau Freixes - Paul Doucet +- Pedro Rodrigues - Peter Darrow - Petr Jašek - Phone Myint Kyaw From 9ac6e723c119297dedf11ab3ab2a1aba6a48f715 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 23 Mar 2019 16:42:28 +0100 Subject: [PATCH 436/821] Changelog for #1240 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4e17b480a..6eee5bce8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Fixed - HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) - Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) -- Documentation typos (`#1218`_) +- Documentation typos (`#1218`_, `#1240`_) Improved ~~~~~~~~ @@ -33,6 +33,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1240`: https://github.com/pyeve/eve/issues/1240 .. _`#1227`: https://github.com/pyeve/eve/issues/1227 .. _`#1231`: https://github.com/pyeve/eve/issues/1231 .. _`#1069`: https://github.com/pyeve/eve/issues/1069 From 61de6d806ac997954dd3784a2ac13d42c919470a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Mar 2019 15:02:14 +0100 Subject: [PATCH 437/821] Bump PyMongo to 3.7+ and fix DeprecationWarnings Adds a last_document_count property to the mongo data layer. This approach does not break the base DataLayer contract. In determining the number of documents we do our best to use the new count_documents() method introduced with PyMongo 3.7. However, count_documents() does not support the $where operator. It must be replaced with $expr, which in turn is not supported by older Mongos (3.4-). Since we do not want to impose a limit on the supported DB version, for the time being we will fallback to the deprecated cursor.count() if needed. See http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.count Closes #1202. --- CHANGES.rst | 3 +++ eve/io/mongo/mongo.py | 38 ++++++++++++++++++++++++++++------- eve/methods/get.py | 6 +++--- eve/tests/auth.py | 13 +++++------- eve/tests/io/flask_pymongo.py | 6 +++--- eve/tests/io/mongo.py | 2 +- eve/tests/io/multi_mongo.py | 2 +- eve/tests/methods/common.py | 11 +++++----- eve/tests/methods/delete.py | 20 +++++++++--------- eve/tests/methods/post.py | 8 ++++---- eve/tests/versioning.py | 6 ++---- setup.py | 2 +- 12 files changed, 68 insertions(+), 49 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6eee5bce8..45df44dd8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ New Fixed ~~~~~ +- DeprecationWarning on PyMongo 3.7+ (`#1202`_) - Expecting JSON response for rate limit exceeded scenario (`#1227`_) - Multiple concurrent patches to the same record, from different processes, should result in at least one patch failing with a 412 error (Precondition @@ -24,6 +25,7 @@ Fixed Improved ~~~~~~~~ +- Bump PyMongo version to v3.7+ (`#1202`_) - Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary inside, and each key-value pair yields the related links for a data relation @@ -33,6 +35,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1202`: https://github.com/pyeve/eve/issues/1202 .. _`#1240`: https://github.com/pyeve/eve/issues/1240 .. _`#1227`: https://github.com/pyeve/eve/issues/1227 .. _`#1231`: https://github.com/pyeve/eve/issues/1231 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index f917a3fd5..b1d9a62fa 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -117,6 +117,7 @@ class Mongo(DataLayer): + ["$geometry", "$maxDistance", "$box"] + ["$all", "$elemMatch", "$size"] + ["$bitsAllClear", "$bitsAllSet", "$bitsAnyClear", "$bitsAnySet"] + + ["$center", "$expr"] ) def init_app(self, app): @@ -249,7 +250,29 @@ def find(self, resource, req, sub_resource_lookup): if projection: args["projection"] = projection - return self.pymongo(resource).db[datasource].find(**args) + result = self.pymongo(resource).db[datasource].find(**args) + + try: + self.last_documents_count = ( + self.pymongo(resource).db[datasource].count_documents(spec) + ) + except: + # fallback to deprecated method. this might happen when the query + # includes operators not supported by count_documents(). one + # documented use-case is when we're running on mongo 3.4 and below, + # which does not support $expr ($expr must replace $where # in + # count_documents()). + + # 1. Mongo 3.6+; $expr: pass + # 2. Mongo 3.6+; $where: pass (via fallback) + # 3. Mongo 3.4; $where: pass (via fallback) + # 4. Mongo 3.4; $expr: fail (operator not supported by db) + + # See: http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.count + + self.last_documents_count = result.count() + + return result def find_one( self, @@ -703,10 +726,11 @@ def query_contains_field(self, query, field_name): return True def is_empty(self, resource): - """ Returns True if resource is empty; False otherwise. If there is no - predefined filter on the resource we're relying on the - db.collection.count(). However, if we do have a predefined filter we - have to fallback on the find() method, which can be much slower. + """ Returns True if resource is empty; False otherwise. If there is + no predefined filter on the resource we're relying on the + db.collection.count_documents. However, if we do have a predefined + filter we have to fallback on the find() method, which can be much + slower. .. versionchanged:: 0.6 Support for multiple databases. @@ -719,7 +743,7 @@ def is_empty(self, resource): if not filter_: # faster, but we can only afford it if there's now predefined # filter on the datasource. - return coll.count() == 0 + return coll.count_documents({}) == 0 else: # fallback on find() since we have a filter to apply. try: @@ -728,7 +752,7 @@ def is_empty(self, resource): del filter_[config.LAST_UPDATED] except: pass - return coll.find(filter_).count() == 0 + return coll.count_documents(filter_) == 0 except pymongo.errors.OperationFailure as e: # see comment in :func:`insert()`. self.app.logger.exception(e) diff --git a/eve/methods/get.py b/eve/methods/get.py index b14c851e9..4a6316319 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -256,7 +256,7 @@ def _perform_find(resource, lookup): if config.OPTIMIZE_PAGINATION_FOR_SPEED: count = None else: - count = cursor.count(with_limit_and_skip=False) + count = app.data.last_documents_count headers.append((config.HEADER_TOTAL_COUNT, count)) if config.DOMAIN[resource]["hateoas"]: @@ -432,7 +432,7 @@ def getitem_internal(resource, **lookup): # build all versions documents = [] - if cursor.count() == 0: + if app.data.last_documents_count == 0: # this is the scenario when the document existed before # document versioning got turned on documents.append(latest_doc) @@ -484,7 +484,7 @@ def getitem_internal(resource, **lookup): if config.DOMAIN[resource]["hateoas"]: # use the id of the latest document for multi-document requests if cursor: - count = cursor.count(with_limit_and_skip=False) + count = app.data.last_documents_count response[config.LINKS] = _pagination_links( resource, req, count, latest_doc[resource_def["id_field"]] ) diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 651df8ffe..411e019ed 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -836,8 +836,8 @@ def test_delete(self): _db = self.connection[MONGO_DBNAME] # make sure that other documents in the collections are untouched. - cursor = _db.contacts.find() - docs_num = cursor.count() + _db.contacts.find() + docs_num = _db.contacts.count_documents({}) _, _ = self.post() @@ -865,15 +865,13 @@ def test_delete(self): self.assertEqual(len(response[self.app.config["ITEMS"]]), 0) # make sure no other document has been deleted. - cursor = _db.contacts.find() - self.assertEqual(cursor.count(), docs_num) + self.assertEqual(_db.contacts.count_documents({}), docs_num) def test_delete_item(self): _db = self.connection[MONGO_DBNAME] # make sure that other documents in the collections are untouched. - cursor = _db.contacts.find() - docs_num = cursor.count() + docs_num = _db.contacts.count_documents({}) data, _ = self.post() @@ -890,8 +888,7 @@ def test_delete_item(self): self.assert204(status) # make sure no other document has been deleted. - cursor = _db.contacts.find() - self.assertEqual(cursor.count(), docs_num) + self.assertEqual(_db.contacts.count_documents({}), docs_num) def post(self): r = self.test_client.post( diff --git a/eve/tests/io/flask_pymongo.py b/eve/tests/io/flask_pymongo.py index b64ca5adc..1b3787fbe 100644 --- a/eve/tests/io/flask_pymongo.py +++ b/eve/tests/io/flask_pymongo.py @@ -29,14 +29,14 @@ def test_auth_params_provided_in_mongo_url(self): ) with self.app.app_context(): db = PyMongo(self.app, "MONGO1").db - self.assertEqual(0, db.works.count()) + self.assertEqual(0, db.works.count_documents({})) def test_auth_params_provided_in_config(self): self.app.config["MONGO1_USERNAME"] = MONGO1_USERNAME self.app.config["MONGO1_PASSWORD"] = MONGO1_PASSWORD with self.app.app_context(): db = PyMongo(self.app, "MONGO1").db - self.assertEqual(0, db.works.count()) + self.assertEqual(0, db.works.count_documents({})) def test_invalid_auth_params_provided(self): # if bad username and/or password is provided in MONGO_URL and mongo @@ -57,7 +57,7 @@ def test_valid_port(self): self.app.config["MONGO1_PORT"] = 27017 with self.app.app_context(): db = PyMongo(self.app, "MONGO1").db - self.assertEqual(0, db.works.count()) + self.assertEqual(0, db.works.count_documents({})) def _setupdb(self): self.connection = MongoClient() diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index 3e6fed495..e148e8918 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -477,7 +477,7 @@ def test_query_contains_field(self): def test_delete_returns_status(self): db = self.connection[MONGO_DBNAME] - count = db.contacts.count() + count = db.contacts.count_documents({}) result = db.contacts.delete_many({}) self.assertEqual(count, result.deleted_count) self.assertEqual(True, result.acknowledged) diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/io/multi_mongo.py index a6925c9b5..49217eb1e 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/io/multi_mongo.py @@ -223,7 +223,7 @@ def test_create_index_with_mongo_uri_and_prefix(self): # check if index was created using MONGO1 prefix db = self.connection[MONGO1_DBNAME] - self.assertTrue("mongodb_features" in db.collection_names()) + self.assertTrue("mongodb_features" in db.list_collection_names()) coll = db["mongodb_features"] indexes = coll.index_information() diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 522efbd4d..0d9a2cbbd 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -533,9 +533,8 @@ def test_post_oplog(self): # however the oplog collection has been updated. db = self.connection[MONGO_DBNAME] - cursor = db.oplog.find() - self.assertEqual(cursor.count(), 1) - self.assertOpLogEntry(cursor[0], "POST") + self.assertEqual(db.oplog.count_documents({}), 1) + self.assertOpLogEntry(db.oplog.find()[0], "POST") class TestOpLogEndpointEnabled(TestOpLogBase): @@ -570,9 +569,9 @@ def oplog_callback(resource, entries): # however the oplog collection has the field. db = self.connection[MONGO_DBNAME] - cursor = db.oplog.find() - self.assertEqual(cursor.count(), 1) - oplog_entry = cursor[0] + db.oplog.find() + self.assertEqual(db.oplog.count_documents({}), 1) + oplog_entry = db.oplog.find()[0] self.assertTrue("extra" in oplog_entry) self.assertTrue("customvalue" in oplog_entry["extra"]["customfield"]) diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index d59b89712..c301f6cc8 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -559,26 +559,24 @@ def test_softdelete_datalayer(self): # show_deleted == True is passed or if the deleted field is part of # the lookup req.show_deleted = False - docs = self.app.data.find(self.known_resource, req, None) - undeleted_count = docs.count() + self.app.data.find(self.known_resource, req, None) + undeleted_count = self.app.data.last_documents_count req.show_deleted = True - docs = self.app.data.find(self.known_resource, req, None) - with_deleted_count = docs.count() - self.assertEqual(undeleted_count, with_deleted_count - 1) + self.app.data.find(self.known_resource, req, None) + self.assertEqual(undeleted_count, self.app.data.last_documents_count - 1) req.show_deleted = False - docs = self.app.data.find( - self.known_resource, req, {self.deleted_field: True} - ) - deleted_count = docs.count() + self.app.data.find(self.known_resource, req, {self.deleted_field: True}) + deleted_count = self.app.data.last_documents_count self.assertEqual(deleted_count, 1) # find_list_of_ids will return deleted documents if given their id - docs = self.app.data.find_list_of_ids( + self.app.data.find_list_of_ids( self.known_resource, [ObjectId(self.item_id)] ) - self.assertEqual(docs.count(), 1) + + self.assertEqual(self.app.data.last_documents_count, 1) def test_softdelete_db_fields(self): """Documents created when soft delete is enabled should include and diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index b1e621d9b..ce613ef0c 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -185,9 +185,9 @@ def test_multi_post_valid(self): with self.app.test_request_context(): contacts = self.app.data.driver.db["contacts"] - r = contacts.find({"ref": "9234567890123456789054321"}).count() + r = contacts.count_documents({"ref": "9234567890123456789054321"}) self.assertTrue(r == 1) - r = contacts.find({"ref": "5432112345678901234567890"}).count() + r = contacts.count_documents({"ref": "5432112345678901234567890"}) self.assertTrue(r == 1) def test_multi_post_invalid(self): @@ -217,9 +217,9 @@ def test_multi_post_invalid(self): with self.app.test_request_context(): contacts = self.app.data.driver.db["contacts"] - r = contacts.find({"prog": 9999}).count() + r = contacts.count_documents({"prog": 9999}) self.assertTrue(r == 0) - r = contacts.find({"ref": "9234567890123456789054321"}).count() + r = contacts.count_documents({"ref": "9234567890123456789054321"}) self.assertTrue(r == 0) def test_post_x_www_form_urlencoded(self): diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 115c90b36..460fa45ce 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -105,16 +105,14 @@ def countDocuments(self, _id=None): if _id is not None: query[self.id_field] = ObjectId(_id) - documents = self._db[self.known_resource].find(query) - return documents.count() + return self._db[self.known_resource].count_documents(query) def countShadowDocuments(self, _id=None): query = {} if _id is not None: query[self.document_id_field] = ObjectId(_id) - documents = self._db[self.known_resource_shadow].find(query) - return documents.count() + return self._db[self.known_resource_shadow].count_documents(query) def assertGoodPutPatch(self, response, status): self.assert200(status) diff --git a/setup.py b/setup.py index b84e52434..519b7ad35 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "cerberus>=1.1", "events>=0.3,<0.4", "flask>=1.0", - "pymongo>=3.5", + "pymongo>=3.7", "simplejson>=3.3.0,<4.0", "werkzeug<=0.14.1", ] From df029adc94e6fcf595905aed71e21732f285280d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Mar 2019 16:12:49 +0100 Subject: [PATCH 438/821] Add support for new query operators to changelog --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 45df44dd8..d593be982 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ Version 0.8.2 New ~~~ - ``on_fetched_diffs`` event hooks (`#1224`_) +- Support for Mongo 3.6+ ``$expr`` query operator. +- Support for Mongo 3.6+ ``$center`` query operator. Fixed ~~~~~ From ad7fc88609123a932b6cd46632e6ae4588b0648a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Mar 2019 18:01:44 +0100 Subject: [PATCH 439/821] Fix: decodestring is deprecated, use decodebytes Closes #1242 --- CHANGES.rst | 4 +++- eve/methods/common.py | 2 +- eve/tests/io/media.py | 17 +++++++---------- eve/tests/methods/get.py | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d593be982..647c5f03e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,7 +14,8 @@ New Fixed ~~~~~ -- DeprecationWarning on PyMongo 3.7+ (`#1202`_) +- DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) +- DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) - Expecting JSON response for rate limit exceeded scenario (`#1227`_) - Multiple concurrent patches to the same record, from different processes, should result in at least one patch failing with a 412 error (Precondition @@ -37,6 +38,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1242`: https://github.com/pyeve/eve/issues/1242 .. _`#1202`: https://github.com/pyeve/eve/issues/1202 .. _`#1240`: https://github.com/pyeve/eve/issues/1240 .. _`#1227`: https://github.com/pyeve/eve/issues/1227 diff --git a/eve/methods/common.py b/eve/methods/common.py index 4f78c31b4..d19f1c4c7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1101,7 +1101,7 @@ def resolve_one_media(file_id, resource): # otherwise we have a valid file and should send extended response # start with the basic file object if config.RETURN_MEDIA_AS_BASE64_STRING: - ret_file = base64.encodestring(_file.read()) + ret_file = base64.b64encode(_file.read()) elif config.RETURN_MEDIA_AS_URL: prefix = ( config.MEDIA_BASE_URL diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 4b5eed3ad..62b0ccddb 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -30,11 +30,8 @@ def setUp(self): self.headers = [("Content-Type", "multipart/form-data")] self.id_field = self.domain[self.resource]["id_field"] self.test_field, self.test_value = "ref", "1234567890123456789054321" - # we want an explicit binary as Py3 encodestring() expects binaries. self.clean = b"my file contents" - # encodedstring will raise a DeprecationWarning under Python3.3, but - # the alternative encodebytes is not available in Python 2. - self.encoded = base64.encodestring(self.clean).decode("utf-8") + self.encoded = base64.b64encode(self.clean).decode() def test_gridfs_media_storage_errors(self): self.assertRaises(TypeError, GridFSMediaStorage) @@ -70,7 +67,7 @@ def test_gridfs_media_storage_post(self): self.assertEqual(returned, self.encoded) # which decodes to the original clean - self.assertEqual(base64.decodestring(returned.encode()), self.clean) + self.assertEqual(base64.b64decode(returned.encode()), self.clean) def test_gridfs_media_storage_post_excluded_file_in_result(self): # send something different than a file and get an error back @@ -122,7 +119,7 @@ def test_gridfs_media_storage_post_extended(self): self.assertEqual(returned["file"], self.encoded) # which decodes to the original clean - self.assertEqual(base64.decodestring(returned["file"].encode()), self.clean) + self.assertEqual(base64.b64decode(returned["file"].encode()), self.clean) # also verify our extended fields self.assertEqual(returned["content_type"], "text/plain") @@ -165,7 +162,7 @@ def test_gridfs_media_storage_put(self): # PUT replaces the file with new one clean = b"my new file contents" - encoded = base64.encodestring(clean).decode() + encoded = base64.b64encode(clean).decode() test_field, test_value = "ref", "9234567890123456789054321" data = {"media": (BytesIO(clean), "test.txt"), test_field: test_value} headers = [("Content-Type", "multipart/form-data"), ("If-Match", etag)] @@ -205,7 +202,7 @@ def test_gridfs_media_storage_patch(self): # PATCH replaces the file with new one clean = b"my new file contents" - encoded = base64.encodestring(clean).decode() + encoded = base64.b64encode(clean).decode() test_field, test_value = "ref", "9234567890123456789054321" data = {"media": (BytesIO(clean), "test.txt"), test_field: test_value} headers = [("Content-Type", "multipart/form-data"), ("If-Match", etag)] @@ -460,7 +457,7 @@ def assertMediaField(self, _id, encoded, clean): # returned value is a base64 encoded string self.assertEqual(returned, encoded) # which decodes to the original file clean - self.assertEqual(base64.decodestring(returned.encode()), clean) + self.assertEqual(base64.b64decode(returned.encode()), clean) return r, s def assertMediaFieldExtended(self, _id, encoded, clean): @@ -470,7 +467,7 @@ def assertMediaFieldExtended(self, _id, encoded, clean): # returned value is a base64 encoded string self.assertEqual(returned, encoded) # which decodes to the original file clean - self.assertEqual(base64.decodestring(returned.encode()), clean) + self.assertEqual(base64.b64decode(returned.encode()), clean) return r, s def assertMediaStored(self, _id): diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 6a2a54a50..b26a1a890 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -819,9 +819,9 @@ def test_get_embedded_media(self): returned = response["image_file"]["file"] # encodedstring will raise a DeprecationWarning under Python3.3, but # the alternative encodebytes is not available in Python 2. - encoded = base64.encodestring(asset).decode("utf-8") + encoded = base64.b64encode(asset).decode("utf-8") self.assertEqual(returned, encoded) - self.assertEqual(base64.decodestring(returned.encode()), asset) + self.assertEqual(base64.b64decode(returned.encode()), asset) def test_get_embedded(self): # We need to assign a `person` to our test invoice From f909209bab66476403d16b1083ce0b3782a0bdce Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Mar 2019 18:07:37 +0100 Subject: [PATCH 440/821] Fix: JSON setting is deprecated. Use RENDERERS instead Closes #1241 --- CHANGES.rst | 2 ++ eve/tests/renders.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 647c5f03e..eef7c301d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ New Fixed ~~~~~ +- UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). - DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) - DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) - Expecting JSON response for rate limit exceeded scenario (`#1227`_) @@ -38,6 +39,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1241`: https://github.com/pyeve/eve/issues/1241 .. _`#1242`: https://github.com/pyeve/eve/issues/1242 .. _`#1202`: https://github.com/pyeve/eve/issues/1202 .. _`#1240`: https://github.com/pyeve/eve/issues/1240 diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 0dda58333..0a84fa2ac 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -369,7 +369,7 @@ def test_CORS_OPTIONS_schema(self): def test_deprecated_renderers_supports_py27(self): """ Make sure #1175 is fixed """ - self.app.config["JSON"] = False + self.app.config["RENDERES"] = False try: self.app.check_deprecated_features() except AttributeError: From ac3bd78a9dfcad38e2fe478c0f35f66ac290c37c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 09:39:41 +0100 Subject: [PATCH 441/821] Performance: run count_documents() only if required Addresses #1202 --- eve/io/mongo/mongo.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b1d9a62fa..b7ea8d60b 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -250,12 +250,19 @@ def find(self, resource, req, sub_resource_lookup): if projection: args["projection"] = projection - result = self.pymongo(resource).db[datasource].find(**args) + self.__last_target = self.pymongo(resource).db[datasource], spec + self.__last_cursor = self.pymongo(resource).db[datasource].find(**args) + + return self.__last_cursor + + @property + def last_documents_count(self): + if not self.__last_target: + return None try: - self.last_documents_count = ( - self.pymongo(resource).db[datasource].count_documents(spec) - ) + target, spec = self.__last_target + return target.count_documents(spec) except: # fallback to deprecated method. this might happen when the query # includes operators not supported by count_documents(). one @@ -269,10 +276,7 @@ def find(self, resource, req, sub_resource_lookup): # 4. Mongo 3.4; $expr: fail (operator not supported by db) # See: http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.count - - self.last_documents_count = result.count() - - return result + return self.__last_cursor.count() def find_one( self, From 2f1741852152b810c149a7e1e82cc8bd6ef43e12 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 10:42:46 +0100 Subject: [PATCH 442/821] Fix: return 400 'immutable field' on Mongo 3.6+ Closes #1243 --- CHANGES.rst | 2 ++ eve/io/mongo/mongo.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index eef7c301d..af35b5838 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ New Fixed ~~~~~ +- On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) - UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). - DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) - DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) @@ -39,6 +40,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1243`: https://github.com/pyeve/eve/issues/1243 .. _`#1241`: https://github.com/pyeve/eve/issues/1241 .. _`#1242`: https://github.com/pyeve/eve/issues/1242 .. _`#1202`: https://github.com/pyeve/eve/issues/1202 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b7ea8d60b..fca08b0a2 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -519,7 +519,8 @@ def _change_request(self, resource, id_, changes, original, replace=False): # server error codes and messages changed between 2.4 and 2.6/3.0. server_version = self.driver.db.client.server_info()["version"][:3] if (server_version == "2.4" and e.code in (13596, 10148)) or ( - server_version in ("2.6", "3.0", "3.2", "3.4") and e.code in (66, 16837) + server_version in ("2.6", "3.0", "3.2", "3.4", "3.6", "4.0") + and e.code in (66, 16837) ): # attempt to update an immutable field. this usually # happens when a PATCH or PUT includes a mismatching ID_FIELD. From 5c3c3a35aea5daa24402dba3c5aa3280d4c49f2f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 09:47:59 +0100 Subject: [PATCH 443/821] Add Python 3.7 to CI matrix Closes #1199 --- .travis.yml | 6 ++++-- CHANGES.rst | 2 ++ setup.py | 1 + tox.ini | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a24e5c05..1f9daa001 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial sudo: false language: python stages: @@ -10,7 +11,8 @@ python: - 3.4 - 3.5 - 3.6 - - pypy + - 3.7 + - pypy3.5-6.0 install: travis_retry pip install tox-travis services: - mongodb @@ -22,7 +24,7 @@ before_script: jobs: include: - stage: linting - python: '3.6' + python: '3.7' env: install: - pip install pre-commit diff --git a/CHANGES.rst b/CHANGES.rst index af35b5838..cd161d78f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,7 @@ New - ``on_fetched_diffs`` event hooks (`#1224`_) - Support for Mongo 3.6+ ``$expr`` query operator. - Support for Mongo 3.6+ ``$center`` query operator. +- Python 3.7 added to the CI matrix (`#1199`_) Fixed ~~~~~ @@ -40,6 +41,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +.. _`#1199`: https://github.com/pyeve/eve/issues/1199 .. _`#1243`: https://github.com/pyeve/eve/issues/1243 .. _`#1241`: https://github.com/pyeve/eve/issues/1241 .. _`#1242`: https://github.com/pyeve/eve/issues/1242 diff --git a/setup.py b/setup.py index 519b7ad35..074a3eace 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", diff --git a/tox.ini b/tox.ini index 7ecf4a452..d8ffa741c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py34,py35,py36,pypy,linting +envlist=py27,py34,py35,py36,py37,pypy,linting [testenv] extras=tests @@ -18,6 +18,7 @@ python = 3.4: py34 3.5: py35 3.6: py36 + 3.7: py37 pypy: pypy [flake8] From 8f691e5f3211164e93b3e7996cccbb71eb981f1d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 11:28:15 +0100 Subject: [PATCH 444/821] Add a "Python 3 is preferred" note on the homepage Closes #1198 --- CHANGES.rst | 2 ++ docs/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd161d78f..58fc5e5cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -40,7 +40,9 @@ Improved ``title`` (`#1204`_) - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) +- Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1198`: https://github.com/pyeve/eve/issues/1198 .. _`#1199`: https://github.com/pyeve/eve/issues/1199 .. _`#1243`: https://github.com/pyeve/eve/issues/1243 .. _`#1241`: https://github.com/pyeve/eve/issues/1241 diff --git a/docs/index.rst b/docs/index.rst index 9b67e0545..02924d687 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -35,6 +35,8 @@ community extensions_. The codebase is thoroughly tested under Python 2.7, 3.4+, and PyPy. +.. note:: The use of **Python 3** is *highly* preferred over Python 2. Consider upgrading your applications and infrastructure if you find yourself *still* using Python 2 in production today. + Eve is Simple ------------- .. code-block:: python From 852c6ce35482752c118d15fdebb5921e182bb611 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 12:05:57 +0100 Subject: [PATCH 445/821] Fix: Soft delete removes auth_field from document Closes #1188 --- CHANGES.rst | 2 ++ eve/methods/delete.py | 8 +++++++- eve/tests/auth.py | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 58fc5e5cd..1d534508b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ New Fixed ~~~~~ +- Soft delete removes ``auth_field`` from document (`#1188`_) - On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) - UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). - DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) @@ -42,6 +43,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1188`: https://github.com/pyeve/eve/issues/1188 .. _`#1198`: https://github.com/pyeve/eve/issues/1198 .. _`#1199`: https://github.com/pyeve/eve/issues/1199 .. _`#1243`: https://github.com/pyeve/eve/issues/1243 diff --git a/eve/methods/delete.py b/eve/methods/delete.py index ad354203a..3ea72bc1b 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -94,7 +94,13 @@ def deleteitem_internal( """ resource_def = config.DOMAIN[resource] soft_delete_enabled = resource_def["soft_delete"] - original = get_document(resource, concurrency_check, original, **lookup) + original = get_document( + resource, + concurrency_check, + original, + force_auth_field_projection=soft_delete_enabled, + **lookup + ) if not original or (soft_delete_enabled and original.get(config.DELETED) is True): abort(404) diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 411e019ed..438eca232 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -890,6 +890,33 @@ def test_delete_item(self): # make sure no other document has been deleted. self.assertEqual(_db.contacts.count_documents({}), docs_num) + def test_delete_item_soft_delete_enabled(self): + self.app.config["DOMAIN"]["restricted"]["soft_delete"] = True + _db = self.connection[MONGO_DBNAME] + docs_num = _db.contacts.count_documents({}) + + data, _ = self.post() + + url = "%s/%s" % (self.url, data["_id"]) + response = self.test_client.get(url, headers=self.valid_auth) + etag = response.headers["ETag"] + headers = [("If-Match", etag), ("Authorization", "Basic YWRtaW46c2VjcmV0")] + + # delete the document + response, status = self.parse_response( + self.test_client.delete(url, headers=headers) + ) + self.assert204(status) + + # make sure no other document has been deleted. + self.assertEqual( + _db.contacts.count_documents({"_deleted": {"$ne": True}}), docs_num + ) + self.assertEqual(_db.contacts.count_documents({"_deleted": True}), 1) + + challenge = _db.contacts.find_one({"_deleted": True}) + self.assertEqual(challenge["username"], "admin") + def post(self): r = self.test_client.post( self.url, From e810686b0486e74897691ae01198432be01d4acf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 27 Mar 2019 22:54:38 +0100 Subject: [PATCH 446/821] Fix: datasource projection should be respected in POST requests Closes #1189 --- CHANGES.rst | 4 +++- eve/methods/common.py | 24 ++++++++++++++++++++++++ eve/tests/methods/post.py | 11 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1d534508b..13cffc93f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,12 +9,13 @@ Version 0.8.2 New ~~~ - ``on_fetched_diffs`` event hooks (`#1224`_) +- Python 3.7 added to the CI matrix (`#1199`_) - Support for Mongo 3.6+ ``$expr`` query operator. - Support for Mongo 3.6+ ``$center`` query operator. -- Python 3.7 added to the CI matrix (`#1199`_) Fixed ~~~~~ +- Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) - On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) - UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). @@ -43,6 +44,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1189`: https://github.com/pyeve/eve/issues/1189 .. _`#1188`: https://github.com/pyeve/eve/issues/1188 .. _`#1198`: https://github.com/pyeve/eve/issues/1198 .. _`#1199`: https://github.com/pyeve/eve/issues/1199 diff --git a/eve/methods/common.py b/eve/methods/common.py index d19f1c4c7..b0bae6b25 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -615,6 +615,8 @@ def build_response_document(document, resource, embedded_fields, latest_doc=None """ resource_def = config.DOMAIN[resource] + resolve_resource_projection(document, resource) + # need to update the document field since the etag must be computed on the # same document representation that might have been used in the collection # 'get' method @@ -664,6 +666,28 @@ def build_response_document(document, resource, embedded_fields, latest_doc=None resolve_embedded_documents(document, resource, embedded_fields) +def resolve_resource_projection(document, resource): + """ Purges a document of fields that are not included in its resource + projecton. + + :param document: the original document. + :param resource: the resource name. + """ + + if config.BANDWIDTH_SAVER: + return + + resource_def = config.DOMAIN[resource] + projection = resource_def["datasource"]["projection"] + fields = { + field for field, value in projection.items() if value and field in document + } + fields.add(resource_def["id_field"]) + + for field in set(document.keys()) - fields: + del (document[field]) + + def field_definition(resource, chained_fields): """ Resolves query string to resource with dot notation like 'people.address.city' and returns corresponding field definition diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index ce613ef0c..a87c11f63 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -944,6 +944,17 @@ def test_post_dont_normalize_dotted_fields(self): # key 'dotted.fields' must not contain '.' self.assertEqual(500, status) + def test_post_projection_is_honored(self): + data = {"ref": "1234567890123456789054321", "aninteger": 100} + self.app.config["BANDWIDTH_SAVER"] = False + self.domain["contacts"]["datasource"]["projection"] = {"ref": 1} + + r, status = self.post(self.known_resource_url, data=data) + self.assert201(status) + self.assertPostResponse(r) + self.assertTrue("ref" in r) + self.assertTrue("aninteger" not in r) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) From 8a21f5be8dbd17694ece6062ca82aef183d1ba8a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 28 Mar 2019 14:49:52 +0100 Subject: [PATCH 447/821] Fix pre-commit warning message for black Close #1244 --- .pre-commit-config.yaml | 2 +- CHANGES.rst | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 367160623..7a20521c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: stable hooks: - id: black - python_version: python3.6 + language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 hooks: diff --git a/CHANGES.rst b/CHANGES.rst index 13cffc93f..ce3e25276 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,9 +18,6 @@ Fixed - Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) - On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) -- UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). -- DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) -- DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) - Expecting JSON response for rate limit exceeded scenario (`#1227`_) - Multiple concurrent patches to the same record, from different processes, should result in at least one patch failing with a 412 error (Precondition @@ -29,6 +26,10 @@ Fixed - HATEOAS ``_links`` seems to get an extra ``&version=diffs`` (`#1228`_) - Do not alter ETag when performing an oplog_push (`#1206`_) - CORS response headers missing for media endpoint (`#1197`_) +- Warning: Unexpected keys present on black: ``python_version`` (`#1244`_) +- UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). +- DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) +- DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) - Documentation typos (`#1218`_, `#1240`_) Improved @@ -44,6 +45,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1244`: https://github.com/pyeve/eve/issues/1244 .. _`#1189`: https://github.com/pyeve/eve/issues/1189 .. _`#1188`: https://github.com/pyeve/eve/issues/1188 .. _`#1198`: https://github.com/pyeve/eve/issues/1198 From bc916bfb0714b29935aa7d5e6ca156e9ff2aa547 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 28 Mar 2019 14:59:28 +0100 Subject: [PATCH 448/821] let black do its magic --- eve/methods/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index b0bae6b25..0d3753aa5 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -685,7 +685,7 @@ def resolve_resource_projection(document, resource): fields.add(resource_def["id_field"]) for field in set(document.keys()) - fields: - del (document[field]) + del document[field] def field_definition(resource, chained_fields): From ca2e9c5c39407754b9b216d8e22a394e9551ba78 Mon Sep 17 00:00:00 2001 From: smeng9 <38666763+smeng9@users.noreply.github.com> Date: Sat, 30 Mar 2019 01:45:21 -0500 Subject: [PATCH 449/821] Fix inconsistent DBRef format between GET and PUT --- eve/io/mongo/mongo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index fca08b0a2..e49525b5b 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -94,7 +94,9 @@ class Mongo(DataLayer): str(v).lower() ], "dbref": lambda value: DBRef( - value["$col"], value["$id"], value["$db"] if "$db" in value else None + value["$col"] if "$col" in value else value["$ref"], + value["$id"], + value["$db"] if "$db" in value else None, ) if value is not None else None, From 2ccca0ae33459a7e57f25fd6b714d42f6efaaa91 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 30 Mar 2019 09:50:39 +0100 Subject: [PATCH 450/821] Changelog for #1247 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ce3e25276..9a74f1141 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ New Fixed ~~~~~ +- Insertion failure when replacing a same document containing dbref (`#1216`_) - Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) - On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) @@ -45,6 +46,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1216`: https://github.com/pyeve/eve/issues/1216 .. _`#1244`: https://github.com/pyeve/eve/issues/1244 .. _`#1189`: https://github.com/pyeve/eve/issues/1189 .. _`#1188`: https://github.com/pyeve/eve/issues/1188 From 01457a60addffff334f946136aea90f4bb29d0aa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 30 Mar 2019 09:51:45 +0100 Subject: [PATCH 451/821] smeng9 --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 68ff7f908..3590054fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` + - Aayush Sarva - Alexander Dietmüller - Alexander Hendorf @@ -183,4 +184,5 @@ Patches and Contributions - kreynen - mmizotin - quentinpraz +- smeng9 - xgdgsc From 04a2202f5887f246b1b6182704c20433dc9706fb Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Thu, 28 Mar 2019 17:23:29 -0700 Subject: [PATCH 452/821] PATCH not working as expected for nested document --- docs/features.rst | 31 +++++++++++++++++++++++++++++++ eve/validation.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index 459359369..ee38d4849 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2282,6 +2282,37 @@ The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline wi The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. +# Special Note on PATCH +~~~~~~~~~~~ +``PATCH`` **cannot** remove a field but only update value of the field. + +Consider the following schema: + +``` +'entity': { + 'name': { + 'type': 'string', + 'required': True }, + 'contact': { + 'type': 'dict', + 'required': True, + 'schema': { + 'phone': { + 'type': 'string', + 'required': False, + 'default': '1234567890' }, + 'email': { + 'type': 'string', + 'required': False, + 'default': 'abc@efg.com' }, + } + } +} +``` + +Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. + + Limitations ~~~~~~~~~~~ ``HATEOAS`` is not available at aggregation endpoints. This should not diff --git a/eve/validation.py b/eve/validation.py index aba7a2143..d3178c457 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -37,7 +37,7 @@ def validate_update(self, document, document_id, persisted_document=None): """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document, update=True) + return super(Validator, self).validate(document, update=True, normalize=True) def validate_replace(self, document, document_id, persisted_document=None): """ Validation method to be invoked when performing a document From 165a777b2ee9fbbf2c43ff9c6f35c8bbca108abc Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Fri, 29 Mar 2019 18:19:39 -0700 Subject: [PATCH 453/821] Add test for the patch modification. --- eve/tests/__init__.py | 3 +++ eve/tests/methods/patch.py | 34 ++++++++++++++++++++++++++++++++++ eve/tests/test_settings.py | 19 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 834b63b63..20936793f 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -477,6 +477,9 @@ def setUp(self, url_converters=None): self.child_products = "child_products" self.child_products_url = "/%s" % self.domain[self.child_products]["url"] + self.test_patch = "test_patch" + self.test_patch_url = "/%s" % self.domain[self.test_patch]["url"] + def response_item(self, response, i=0): if self.app.config["HATEOAS"]: return response["_items"][i] diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 5d90bbab5..6537e6d83 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -13,6 +13,40 @@ class TestPatch(TestBase): + def test_patch_not_override_other_fields(self): + # create a data + r, status = self.post(self.test_patch_url, data={"name": "name"}) + self.assert201(status) + # check the data is created correctly + data, status = self.get(self.test_patch_url, item=r._id) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "default_phone") + self.assertEqual(data["email"].get("email", None), "default_email") + + # patch the data + _, status = self.patch(self.test_patch_url + "/" + data._id, data={"contact.phone": "new_phone"}) + self.assert200(status) + # other fields should not be touched + data, status = self.get(self.test_patch_url, item=r._id) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "new_phone") + self.assertEqual(data["email"].get("email", None), "default_email") + + # patch other field of the data + _, status = self.patch(self.test_patch_url + "/" + data._id, data={"contact.email": "new_email"}) + self.assert200(status) + # other fields should not be touched + data, status = self.get(self.test_patch_url, item=r._id) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "new_phone") + self.assertEqual(data["email"].get("email", None), "new_email") + def test_patch_to_resource_endpoint(self): _, status = self.patch(self.known_resource_url, data={}) self.assert405(status) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 02e47dc59..2a359d7e5 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -243,6 +243,24 @@ "parent_product": {"type": "string", "data_relation": {"resource": "products"}}, }, } + +test_patch = { + 'datasource': { + 'source': 'test_patch', + }, + 'schema': { + 'name': {'type': 'string', 'required': True}, + 'contact': { + 'type': 'dict', + 'required': True, + 'schema': { + 'phone': {'type': 'string', 'required': False, 'default': 'default_phone'}, + 'email': {'type': 'string', 'required': False, 'default': 'default_email'}, + } + } + } +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -276,4 +294,5 @@ "products": products, "child_products": child_products, "exclusion": exclusion, + "test_patch": test_patch, } From b00df9a9a485b7e2ccb5516527772aad745fb8e0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 30 Mar 2019 09:58:33 +0100 Subject: [PATCH 454/821] Changelog for #1246 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9a74f1141..3b6cacfa2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ New Fixed ~~~~~ +- PATCH not working as expected for nested document (`#1234`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) - Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) @@ -46,6 +47,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1216`: https://github.com/pyeve/eve/issues/1216 .. _`#1244`: https://github.com/pyeve/eve/issues/1244 .. _`#1189`: https://github.com/pyeve/eve/issues/1189 From 1f3f00609bee434a732cdbcb84e34cc1c0077c40 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 30 Mar 2019 09:59:26 +0100 Subject: [PATCH 455/821] black formatting fixes --- eve/tests/methods/patch.py | 8 ++++++-- eve/tests/test_settings.py | 32 +++++++++++++++++++------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 6537e6d83..611f52c32 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -26,7 +26,9 @@ def test_patch_not_override_other_fields(self): self.assertEqual(data["email"].get("email", None), "default_email") # patch the data - _, status = self.patch(self.test_patch_url + "/" + data._id, data={"contact.phone": "new_phone"}) + _, status = self.patch( + self.test_patch_url + "/" + data._id, data={"contact.phone": "new_phone"} + ) self.assert200(status) # other fields should not be touched data, status = self.get(self.test_patch_url, item=r._id) @@ -37,7 +39,9 @@ def test_patch_not_override_other_fields(self): self.assertEqual(data["email"].get("email", None), "default_email") # patch other field of the data - _, status = self.patch(self.test_patch_url + "/" + data._id, data={"contact.email": "new_email"}) + _, status = self.patch( + self.test_patch_url + "/" + data._id, data={"contact.email": "new_email"} + ) self.assert200(status) # other fields should not be touched data, status = self.get(self.test_patch_url, item=r._id) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 2a359d7e5..3c988a843 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -245,20 +245,26 @@ } test_patch = { - 'datasource': { - 'source': 'test_patch', + "datasource": {"source": "test_patch"}, + "schema": { + "name": {"type": "string", "required": True}, + "contact": { + "type": "dict", + "required": True, + "schema": { + "phone": { + "type": "string", + "required": False, + "default": "default_phone", + }, + "email": { + "type": "string", + "required": False, + "default": "default_email", + }, + }, + }, }, - 'schema': { - 'name': {'type': 'string', 'required': True}, - 'contact': { - 'type': 'dict', - 'required': True, - 'schema': { - 'phone': {'type': 'string', 'required': False, 'default': 'default_phone'}, - 'email': {'type': 'string', 'required': False, 'default': 'default_email'}, - } - } - } } child_products = copy.deepcopy(products) From 7691d55644ed6261ffb784983412032b8c2768b2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 1 Apr 2019 10:29:46 +0200 Subject: [PATCH 456/821] Fix: eve crashes on malformed sort parameters Closes #1248 --- CHANGES.rst | 2 ++ eve/io/mongo/mongo.py | 9 ++++++++- eve/tests/methods/get.py | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3b6cacfa2..4192477da 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ New Fixed ~~~~~ +- Eve crashes on malformed sort parameters (`#1248`_) - PATCH not working as expected for nested document (`#1234`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) - Datasource projection is not respected for POST requests (`#1189`_) @@ -47,6 +48,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1248`: https://github.com/pyeve/eve/issues/1248 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1216`: https://github.com/pyeve/eve/issues/1216 .. _`#1244`: https://github.com/pyeve/eve/issues/1244 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index e49525b5b..5361fc0c4 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -253,7 +253,14 @@ def find(self, resource, req, sub_resource_lookup): args["projection"] = projection self.__last_target = self.pymongo(resource).db[datasource], spec - self.__last_cursor = self.pymongo(resource).db[datasource].find(**args) + try: + self.__last_cursor = self.pymongo(resource).db[datasource].find(**args) + except TypeError as e: + # pymongo raises ValueError when invalid query paramenters are + # included. We do our best to catch them beforehand but, especially + # with key/value sort syntax, invalid ones might still slip in. + self.app.logger.exception(e) + abort(400, description=debug_error_message(str(e))) return self.__last_cursor diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index b26a1a890..fd8601a2b 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1151,6 +1151,8 @@ def test_get_invalid_sort_syntax(self): """ test that invalid sort syntax returns a 400 """ response, status = self.get(self.known_resource, '?sort=[("prog":1)]') self.assert400(status) + response, status = self.get(self.known_resource, '?sort="firstname"') + self.assert400(status) def test_get_allowed_filters_operators(self): """ test that supported operators are not considered invalid filters From 2ba341fdc6f1c0623a5fdf2f08c2b5bf11408524 Mon Sep 17 00:00:00 2001 From: tilla1145 Date: Fri, 28 Dec 2018 23:29:09 +0100 Subject: [PATCH 457/821] Added fix for embedded documents not being sorted correctly. --- eve/methods/common.py | 26 ++++++++++++-------------- eve/tests/methods/common.py | 8 ++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 0d3753aa5..6ab4ca135 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -951,27 +951,25 @@ def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): return temp_embedded_docs -def sort_per_resource(embedded_docs, id_value_to_sort, id_field_name): +def sort_per_resource(embedded_docs, id_values_to_sort, id_field_name): """ Sorts the documents fetched from the database per single resource - :param embedded_docs: the documents fetch from the database. - :param id_value_to_sort: id_value sort criteria. + :param embedded_docs: list of the documents fetched from the database. + :param id_values_to_sort: list of the id_values sort criteria. :param list_of_id_field_name: list of name of fields + :param id_field_name: key name of the id field; `_id` :return embedded_docs: the list of documents sorted as per input """ - # Removing None - number_of_none = embedded_docs.count(None) - if number_of_none: - embedded_docs = [x for x in embedded_docs if x is not None] + if isinstance(id_values_to_sort, list) and id_values_to_sort is None: + id_values_to_sort = [] + embedded_docs = [x for x in embedded_docs if x is not None] id2dict = dict((d[id_field_name], d) for d in embedded_docs) temporary_embedded_docs = [] - if number_of_none: - for id_value_ in id_value_to_sort: - if id_value_ in id2dict: - temporary_embedded_docs.append(id2dict[id_value_]) - else: - temporary_embedded_docs.append(None) - return embedded_docs + for id_value_ in id_values_to_sort: + if id_value_ in id2dict: + temporary_embedded_docs.append(id2dict[id_value_]) + + return temporary_embedded_docs def generate_query_and_sorting_criteria(data_relation, references): diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 0d9a2cbbd..5187ae15f 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -759,3 +759,11 @@ def test_ticket_681(self): # See https://github.com/pyeve/eve/issues/681 with self.app.test_request_context("not_an_existing_endpoint"): self.app.data.driver.db["again"] + + +class TestEmbeddedDocuments(TestBase): + def setUp(self, url_converters=None): + super(TestEmbeddedDocuments, self).setUp() + + def test_sort_per_resource_embedded_docs(self): + pass From 6788f30fc9dd5648fad308362d775914dacb0276 Mon Sep 17 00:00:00 2001 From: Mamur Date: Mon, 1 Apr 2019 22:51:56 +0200 Subject: [PATCH 458/821] Removed type check for id_values_to_sort which would cause always falsy if clause. --- eve/methods/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 6ab4ca135..72f3802e2 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -960,7 +960,7 @@ def sort_per_resource(embedded_docs, id_values_to_sort, id_field_name): :param id_field_name: key name of the id field; `_id` :return embedded_docs: the list of documents sorted as per input """ - if isinstance(id_values_to_sort, list) and id_values_to_sort is None: + if id_values_to_sort is None: id_values_to_sort = [] embedded_docs = [x for x in embedded_docs if x is not None] id2dict = dict((d[id_field_name], d) for d in embedded_docs) From 6fd19e61708dd4f2f1de98ca016207f1352f4bbb Mon Sep 17 00:00:00 2001 From: Mamur Date: Tue, 2 Apr 2019 00:21:55 +0200 Subject: [PATCH 459/821] Added trivial test for TestEmbeddedDocuments. --- eve/tests/methods/common.py | 13 +++++++++---- eve/tests/suite_generator.py | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 eve/tests/suite_generator.py diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 5187ae15f..4adc5dfd8 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -1,11 +1,11 @@ import time from datetime import datetime - +from random import shuffle import simplejson as json from bson import ObjectId, decimal128 from bson.dbref import DBRef - -from eve.methods.common import serialize, normalize_dotted_fields +from eve.tests.suite_generator import EmbeddedDoc +from eve.methods.common import serialize, normalize_dotted_fields, sort_per_resource from eve.tests import TestBase from eve.tests.auth import ValidBasicAuth, ValidTokenAuth, ValidHMACAuth from eve.tests.test_settings import MONGO_DBNAME @@ -766,4 +766,9 @@ def setUp(self, url_converters=None): super(TestEmbeddedDocuments, self).setUp() def test_sort_per_resource_embedded_docs(self): - pass + object_ids = [ObjectId() for _ in range(8)] + embedded_docs = [EmbeddedDoc(_id=_id).__dict__ for _id in object_ids] + + shuffle(object_ids) + sorted_docs = sort_per_resource(embedded_docs, object_ids[:7], "_id") + self.assertEqual(len(sorted_docs), 7) diff --git a/eve/tests/suite_generator.py b/eve/tests/suite_generator.py new file mode 100644 index 000000000..ec1c0dd2d --- /dev/null +++ b/eve/tests/suite_generator.py @@ -0,0 +1,7 @@ +from datetime import datetime + + +class EmbeddedDoc: + def __init__(self, _id): + self._id = _id + self._created = datetime.utcnow() From e5d66996d8942977ae9fb06c68c6f027f09aa2bd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Apr 2019 10:31:33 +0200 Subject: [PATCH 460/821] Changelog for #1217 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4192477da..145025fd5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ New Fixed ~~~~~ +- Embedded documents not being sorted correctly (`#1217`_) - Eve crashes on malformed sort parameters (`#1248`_) - PATCH not working as expected for nested document (`#1234`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) @@ -48,6 +49,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1217`: https://github.com/pyeve/eve/pull/1217 .. _`#1248`: https://github.com/pyeve/eve/issues/1248 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1216`: https://github.com/pyeve/eve/issues/1216 From 52a2bae56bfb479dc5b2fa5e261669fb20587aae Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Apr 2019 10:32:54 +0200 Subject: [PATCH 461/821] Mamurjon Saitbaev --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 3590054fd..874c361e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -103,6 +103,7 @@ Patches and Contributions - Luca Moretto - Luis Fernando Gomes - Magdas Adrian +- Mamurjon Saitbaev - Mandar Vaze - Manquer - Marc Abramowitz From dedace777ad3a1e70e111cad7b54a5fc20e39186 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Apr 2019 16:04:22 +0200 Subject: [PATCH 462/821] Revert "Merge branch 'pull_#1246'" This reverts commit 9b2ba0f2b742e6092ec21ba024049b2362a03a55, reversing changes made to 5872c557041fa53911944ac0a623f6db46686536. --- CHANGES.rst | 1 - docs/features.rst | 31 ------------------------------- eve/tests/__init__.py | 3 --- eve/tests/methods/patch.py | 38 -------------------------------------- eve/tests/test_settings.py | 25 ------------------------- eve/validation.py | 2 +- 6 files changed, 1 insertion(+), 99 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 145025fd5..d7732fcdf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,6 @@ Fixed ~~~~~ - Embedded documents not being sorted correctly (`#1217`_) - Eve crashes on malformed sort parameters (`#1248`_) -- PATCH not working as expected for nested document (`#1234`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) - Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) diff --git a/docs/features.rst b/docs/features.rst index ee38d4849..459359369 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2282,37 +2282,6 @@ The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline wi The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. -# Special Note on PATCH -~~~~~~~~~~~ -``PATCH`` **cannot** remove a field but only update value of the field. - -Consider the following schema: - -``` -'entity': { - 'name': { - 'type': 'string', - 'required': True }, - 'contact': { - 'type': 'dict', - 'required': True, - 'schema': { - 'phone': { - 'type': 'string', - 'required': False, - 'default': '1234567890' }, - 'email': { - 'type': 'string', - 'required': False, - 'default': 'abc@efg.com' }, - } - } -} -``` - -Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. - - Limitations ~~~~~~~~~~~ ``HATEOAS`` is not available at aggregation endpoints. This should not diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 20936793f..834b63b63 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -477,9 +477,6 @@ def setUp(self, url_converters=None): self.child_products = "child_products" self.child_products_url = "/%s" % self.domain[self.child_products]["url"] - self.test_patch = "test_patch" - self.test_patch_url = "/%s" % self.domain[self.test_patch]["url"] - def response_item(self, response, i=0): if self.app.config["HATEOAS"]: return response["_items"][i] diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 611f52c32..5d90bbab5 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -13,44 +13,6 @@ class TestPatch(TestBase): - def test_patch_not_override_other_fields(self): - # create a data - r, status = self.post(self.test_patch_url, data={"name": "name"}) - self.assert201(status) - # check the data is created correctly - data, status = self.get(self.test_patch_url, item=r._id) - self.assert200(status) - self.assertEqual(data.get("name", None), "name") - self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "default_phone") - self.assertEqual(data["email"].get("email", None), "default_email") - - # patch the data - _, status = self.patch( - self.test_patch_url + "/" + data._id, data={"contact.phone": "new_phone"} - ) - self.assert200(status) - # other fields should not be touched - data, status = self.get(self.test_patch_url, item=r._id) - self.assert200(status) - self.assertEqual(data.get("name", None), "name") - self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "new_phone") - self.assertEqual(data["email"].get("email", None), "default_email") - - # patch other field of the data - _, status = self.patch( - self.test_patch_url + "/" + data._id, data={"contact.email": "new_email"} - ) - self.assert200(status) - # other fields should not be touched - data, status = self.get(self.test_patch_url, item=r._id) - self.assert200(status) - self.assertEqual(data.get("name", None), "name") - self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "new_phone") - self.assertEqual(data["email"].get("email", None), "new_email") - def test_patch_to_resource_endpoint(self): _, status = self.patch(self.known_resource_url, data={}) self.assert405(status) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 3c988a843..02e47dc59 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -243,30 +243,6 @@ "parent_product": {"type": "string", "data_relation": {"resource": "products"}}, }, } - -test_patch = { - "datasource": {"source": "test_patch"}, - "schema": { - "name": {"type": "string", "required": True}, - "contact": { - "type": "dict", - "required": True, - "schema": { - "phone": { - "type": "string", - "required": False, - "default": "default_phone", - }, - "email": { - "type": "string", - "required": False, - "default": "default_email", - }, - }, - }, - }, -} - child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -300,5 +276,4 @@ "products": products, "child_products": child_products, "exclusion": exclusion, - "test_patch": test_patch, } diff --git a/eve/validation.py b/eve/validation.py index d3178c457..aba7a2143 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -37,7 +37,7 @@ def validate_update(self, document, document_id, persisted_document=None): """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document, update=True, normalize=True) + return super(Validator, self).validate(document, update=True) def validate_replace(self, document, document_id, persisted_document=None): """ Validation method to be invoked when performing a document From 63ddfa8181cc5b108b0ece154e0177989dbc519b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 3 Apr 2019 11:03:49 +0200 Subject: [PATCH 463/821] Unauthorized Exception not working with Werkzeug >= 15.0 Closes #1245 --- CHANGES.rst | 8 ++++++++ eve/auth.py | 24 +++++------------------- eve/endpoints.py | 17 ++++++++++++----- eve/io/base.py | 6 +++++- eve/tests/auth.py | 5 ++++- setup.py | 2 +- 6 files changed, 35 insertions(+), 27 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d7732fcdf..a81911b53 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,11 @@ Here you can see the full list of changes between each Eve release. Version 0.8.2 ------------- +Breaking changes +~~~~~~~~~~~~~~~~ +- Werkzeug v0.15.1+ is required. You want to upgrade, otherwise your Eve + environment is likely to break. For the full story, see `#1245`_. + New ~~~ - ``on_fetched_diffs`` event hooks (`#1224`_) @@ -15,6 +20,7 @@ New Fixed ~~~~~ +- Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_) - Embedded documents not being sorted correctly (`#1217`_) - Eve crashes on malformed sort parameters (`#1248`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) @@ -37,6 +43,7 @@ Fixed Improved ~~~~~~~~ +- Bump Werkzeug version to v0.15.1+ (`#1245`_) - Bump PyMongo version to v3.7+ (`#1202`_) - Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary @@ -48,6 +55,7 @@ Improved their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +.. _`#1245`: https://github.com/pyeve/eve/pull/1245 .. _`#1217`: https://github.com/pyeve/eve/pull/1217 .. _`#1248`: https://github.com/pyeve/eve/issues/1248 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 diff --git a/eve/auth.py b/eve/auth.py index 122d9af6f..f89244e04 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -9,7 +9,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -from flask import request, Response, current_app as app, g, abort +from flask import request, current_app as app, g, abort from functools import wraps @@ -146,10 +146,11 @@ def authenticate(self): """ Returns a standard a 401 response that enables basic auth. Override if you want to change the response and/or the realm. """ - resp = Response( - None, 401, {"WWW-Authenticate": 'Basic realm="%s"' % __package__} + abort( + 401, + "Please provide proper credentials", + ("WWW-Authenticate", 'Basic realm="%s"' % __package__), ) - abort(401, description="Please provide proper credentials", response=resp) def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. @@ -202,12 +203,6 @@ def check_auth( """ raise NotImplementedError - def authenticate(self): - """ Returns a standard a 401. Override if you want to change the - response. - """ - abort(401, description="Please provide proper credentials") - def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. @@ -260,15 +255,6 @@ def check_auth(self, token, allowed_roles, resource, method): """ raise NotImplementedError - def authenticate(self): - """ Returns a standard a 401. Override if you want to change the - response. - """ - resp = Response( - None, 401, {"WWW-Authenticate": 'Basic realm="%s"' % __package__} - ) - abort(401, description="Please provide proper credentials", response=resp) - def authorized(self, allowed_roles, resource, method): """ Validates the the current request is allowed to pass through. diff --git a/eve/endpoints.py b/eve/endpoints.py index 2a66dc057..536ce9ee6 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -161,12 +161,19 @@ def home_endpoint(): def error_endpoint(error): """ Response returned when an error is raised by the API (e.g. my means of an abort(4xx). - - .. versionadded:: 0.4 """ - headers = None - if error.response: - headers = error.response.headers + headers = [] + + try: + headers.append(error.response.headers) + except AttributeError: + pass + + try: + headers.append(error.www_authenticate) + except AttributeError: + pass + response = { config.STATUS: config.STATUS_ERR, config.ERROR: {"code": error.code, "message": error.description}, diff --git a/eve/io/base.py b/eve/io/base.py index 650b5fc65..26ee6077c 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -493,7 +493,11 @@ def _datasource_ex( != request_auth_value ): desc = "Incompatible User-Restricted Resource " "request." - abort(401, description=desc) + abort( + 401, + desc, + ("WWW-Authenticate", 'Basic realm="%s"' % __package__), + ) else: query = self.app.data.combine_queries( query, {auth_field: request_auth_value} diff --git a/eve/tests/auth.py b/eve/tests/auth.py index 438eca232..d5c4c8917 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -66,7 +66,10 @@ def setUp(self): ("Authorization", "Basic YWRtaW46c2VjcmV0"), self.content_type, ] - self.invalid_auth = [("Authorization", "Basic IDontThinkSo"), self.content_type] + self.invalid_auth = [ + ("Authorization", "Basic YWRtaW46c2VjcmV1"), + self.content_type, + ] self.valid_media_auth = [ ("Authorization", "Basic YWRtaW46c2VjcmV0"), ("Content-Type", "multipart/form-data"), diff --git a/setup.py b/setup.py index 074a3eace..858e8c143 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ "flask>=1.0", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", - "werkzeug<=0.14.1", + "werkzeug>=0.15.1", ] EXTRAS_REQUIRE = { From 911a72fc7a199298e35e34647b6bce031eef1733 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 3 Apr 2019 11:49:51 +0200 Subject: [PATCH 464/821] Drop sphinx-contrib-embedly when building docs --- CHANGES.rst | 1 + docs/conf.py | 10 +--------- docs/rest_api_for_humans.rst | 3 ++- setup.py | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a81911b53..12eaea14b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -54,6 +54,7 @@ Improved - Make the parsing of ``req.sort`` and ``req.where`` easily reusable by moving their logic to dedicated methods (`#1194`_) - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) +- Drop sphinx-contrib-embedly when building docs. .. _`#1245`: https://github.com/pyeve/eve/pull/1245 .. _`#1217`: https://github.com/pyeve/eve/pull/1217 diff --git a/docs/conf.py b/docs/conf.py index 4cb4e3ae0..4e11326d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,15 +28,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "alabaster", - "sphinxcontrib.embedly", -] - -# sphinxcontrib.embedly -embedly_key = "76207aa23dde489bba6bcbc9e56193a6" +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "alabaster"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/rest_api_for_humans.rst b/docs/rest_api_for_humans.rst index 5f858be12..f9b6cf595 100644 --- a/docs/rest_api_for_humans.rst +++ b/docs/rest_api_for_humans.rst @@ -6,7 +6,8 @@ rundown on Eve features, along with a few code snippets and examples. Hopefully it will do a good job in letting you decide whether Eve is valid solution for your use case. -.. embedly:: https://speakerdeck.com/nicola/eve-rest-api-for-humans +- `REST API for Humans @ SpeakerDeck `_ + Conferences ------------ diff --git a/setup.py b/setup.py index 858e8c143..2d1a1517e 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ ] EXTRAS_REQUIRE = { - "docs": ["sphinx", "alabaster", "sphinxcontrib-embedly"], + "docs": ["sphinx", "alabaster"], "tests": ["redis", "testfixtures", "pytest", "tox"], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] From 26be34984182447b546810b0b08a797c34ee019b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Dietm=C3=BCller?= Date: Wed, 3 Apr 2019 15:06:33 +0200 Subject: [PATCH 465/821] Do not crash without www-authentication header --- eve/endpoints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/endpoints.py b/eve/endpoints.py index 536ce9ee6..78523df97 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -170,7 +170,8 @@ def error_endpoint(error): pass try: - headers.append(error.www_authenticate) + if error.www_authenticate != (None,): + headers.append(error.www_authenticate) except AttributeError: pass From 22746c7c5a7880db5c3aa063f8486bd8167e2f25 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 3 Apr 2019 15:46:31 +0200 Subject: [PATCH 466/821] Changelog for #1251 --- CHANGES.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 12eaea14b..662803d52 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,7 @@ Version 0.8.2 Breaking changes ~~~~~~~~~~~~~~~~ - Werkzeug v0.15.1+ is required. You want to upgrade, otherwise your Eve - environment is likely to break. For the full story, see `#1245`_. + environment is likely to break. For the full story, see `#1245`_ and `#1251`_. New ~~~ @@ -20,7 +20,7 @@ New Fixed ~~~~~ -- Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_) +- Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_, `#1251`_) - Embedded documents not being sorted correctly (`#1217`_) - Eve crashes on malformed sort parameters (`#1248`_) - Insertion failure when replacing a same document containing dbref (`#1216`_) @@ -43,7 +43,7 @@ Fixed Improved ~~~~~~~~ -- Bump Werkzeug version to v0.15.1+ (`#1245`_) +- Bump Werkzeug version to v0.15.1+ (`#1245`_, `#1251`_) - Bump PyMongo version to v3.7+ (`#1202`_) - Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary @@ -56,6 +56,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1251`: https://github.com/pyeve/eve/pull/1251 .. _`#1245`: https://github.com/pyeve/eve/pull/1245 .. _`#1217`: https://github.com/pyeve/eve/pull/1217 .. _`#1248`: https://github.com/pyeve/eve/issues/1248 From b1ece7602b41780b6f0a4478b2b613137763e983 Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Thu, 28 Mar 2019 17:23:29 -0700 Subject: [PATCH 467/821] PATCH not working as expected for nested document Addresses #1234 --- docs/features.rst | 31 ++++++++++++++++++++++++++++++ eve/tests/__init__.py | 3 +++ eve/tests/methods/patch.py | 39 ++++++++++++++++++++++++++++++++++++++ eve/tests/test_settings.py | 25 ++++++++++++++++++++++++ eve/validation.py | 2 +- 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index 459359369..ee38d4849 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2282,6 +2282,37 @@ The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline wi The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. +# Special Note on PATCH +~~~~~~~~~~~ +``PATCH`` **cannot** remove a field but only update value of the field. + +Consider the following schema: + +``` +'entity': { + 'name': { + 'type': 'string', + 'required': True }, + 'contact': { + 'type': 'dict', + 'required': True, + 'schema': { + 'phone': { + 'type': 'string', + 'required': False, + 'default': '1234567890' }, + 'email': { + 'type': 'string', + 'required': False, + 'default': 'abc@efg.com' }, + } + } +} +``` + +Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. + + Limitations ~~~~~~~~~~~ ``HATEOAS`` is not available at aggregation endpoints. This should not diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 834b63b63..20936793f 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -477,6 +477,9 @@ def setUp(self, url_converters=None): self.child_products = "child_products" self.child_products_url = "/%s" % self.domain[self.child_products]["url"] + self.test_patch = "test_patch" + self.test_patch_url = "/%s" % self.domain[self.test_patch]["url"] + def response_item(self, response, i=0): if self.app.config["HATEOAS"]: return response["_items"][i] diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 5d90bbab5..ac0e9fdd7 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -13,6 +13,45 @@ class TestPatch(TestBase): + def test_patch_not_override_other_fields(self): + self.app.config["ENFORCE_IF_MATCH"] = False + # create a data + r, status = self.post(self.test_patch_url, data={"name": "name", "contact": {}}) + self.assert201(status) + # check the data is created correctly + data, status = self.get(self.test_patch, item=r["_id"]) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "default_phone") + self.assertEqual(data["contact"].get("email", None), "default_email") + + # patch the data + _, status = self.patch( + self.test_patch_url + "/" + data["_id"], data={"contact.phone": "new_phone"} + ) + self.assert200(status) + # other fields should not be touched + data, status = self.get(self.test_patch, item=r["_id"]) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "new_phone") + self.assertEqual(data["contact"].get("email", None), "default_email") + + # patch other field of the data + _, status = self.patch( + self.test_patch_url + "/" + data["_id"], data={"contact.email": "new_email"} + ) + self.assert200(status) + # other fields should not be touched + data, status = self.get(self.test_patch, item=r["_id"]) + self.assert200(status) + self.assertEqual(data.get("name", None), "name") + self.assertTrue("contact" in data) + self.assertEqual(data["contact"].get("phone", None), "new_phone") + self.assertEqual(data["contact"].get("email", None), "new_email") + def test_patch_to_resource_endpoint(self): _, status = self.patch(self.known_resource_url, data={}) self.assert405(status) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 02e47dc59..3c988a843 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -243,6 +243,30 @@ "parent_product": {"type": "string", "data_relation": {"resource": "products"}}, }, } + +test_patch = { + "datasource": {"source": "test_patch"}, + "schema": { + "name": {"type": "string", "required": True}, + "contact": { + "type": "dict", + "required": True, + "schema": { + "phone": { + "type": "string", + "required": False, + "default": "default_phone", + }, + "email": { + "type": "string", + "required": False, + "default": "default_email", + }, + }, + }, + }, +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -276,4 +300,5 @@ "products": products, "child_products": child_products, "exclusion": exclusion, + "test_patch": test_patch, } diff --git a/eve/validation.py b/eve/validation.py index aba7a2143..d3178c457 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -37,7 +37,7 @@ def validate_update(self, document, document_id, persisted_document=None): """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document, update=True) + return super(Validator, self).validate(document, update=True, normalize=True) def validate_replace(self, document, document_id, persisted_document=None): """ Validation method to be invoked when performing a document From 0c1a2c10abe98d5cee6c5747000ed42bde32581a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Apr 2019 11:03:21 +0200 Subject: [PATCH 468/821] PATCH incorrectly normalizes default values in sub-documents The approach attempted in b1ece7602 does not take into consideration the existing Validator._normalize_default method which already takes care of defualt values normalization. _normalize_default, however, did not take into consideration subdocument fields. This commit takes care of that while preserving the test introduced in b1ece7602, with small refactoring. Closes #1234 --- eve/tests/methods/patch.py | 16 ++++++---------- eve/validation.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index ac0e9fdd7..b2b898703 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -21,10 +21,6 @@ def test_patch_not_override_other_fields(self): # check the data is created correctly data, status = self.get(self.test_patch, item=r["_id"]) self.assert200(status) - self.assertEqual(data.get("name", None), "name") - self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "default_phone") - self.assertEqual(data["contact"].get("email", None), "default_email") # patch the data _, status = self.patch( @@ -34,10 +30,10 @@ def test_patch_not_override_other_fields(self): # other fields should not be touched data, status = self.get(self.test_patch, item=r["_id"]) self.assert200(status) - self.assertEqual(data.get("name", None), "name") + self.assertEqual(data["name"], "name") self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "new_phone") - self.assertEqual(data["contact"].get("email", None), "default_email") + self.assertEqual(data["contact"]["phone"], "new_phone") + self.assertEqual(data["contact"]["email"], "default_email") # patch other field of the data _, status = self.patch( @@ -47,10 +43,10 @@ def test_patch_not_override_other_fields(self): # other fields should not be touched data, status = self.get(self.test_patch, item=r["_id"]) self.assert200(status) - self.assertEqual(data.get("name", None), "name") + self.assertEqual(data["name"], "name") self.assertTrue("contact" in data) - self.assertEqual(data["contact"].get("phone", None), "new_phone") - self.assertEqual(data["contact"].get("email", None), "new_email") + self.assertEqual(data["contact"]["phone"], "new_phone") + self.assertEqual(data["contact"]["email"], "new_email") def test_patch_to_resource_endpoint(self): _, status = self.patch(self.known_resource_url, data={}) diff --git a/eve/validation.py b/eve/validation.py index d3178c457..20abae38e 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -37,7 +37,7 @@ def validate_update(self, document, document_id, persisted_document=None): """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document, update=True, normalize=True) + return super(Validator, self).validate(document, update=True) def validate_replace(self, document, document_id, persisted_document=None): """ Validation method to be invoked when performing a document @@ -59,7 +59,13 @@ def validate_replace(self, document, document_id, persisted_document=None): def _normalize_default(self, mapping, schema, field): """ {'nullable': True} """ - if not self.persisted_document or field not in self.persisted_document: + + challenge = self.persisted_document + if challenge: + for sub_field in self.document_path: + challenge = challenge[sub_field] + + if not challenge or field not in challenge: super(Validator, self)._normalize_default(mapping, schema, field) def _normalize_default_setter(self, mapping, schema, field): From 2d5162dacad7edc552d4176ca63a6c366ca420c9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Apr 2019 11:36:10 +0200 Subject: [PATCH 469/821] Improve PATCH documentation --- docs/features.rst | 68 ++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index ee38d4849..88bf9e946 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -734,6 +734,43 @@ a matter of fact, Eve's MongoDB data-layer itself extends Cerberus validation by implementing the ``unique`` schema field constraint. For more information see :ref:`validation`. +Editing a Document (PATCH) +-------------------------- +Clients can edit a document with the ``PATCH`` method, while ``PUT`` will +replace it. ``PATCH`` cannot remove a field, but only update its value. + +Consider the following schema: + +.. code-block:: javascript + + 'entity': { + 'name': { + 'type': 'string', + 'required': True + }, + 'contact': { + 'type': 'dict', + 'required': True, + 'schema': { + 'phone': { + 'type': 'string', + 'required': False, + 'default': '1234567890' + }, + 'email': { + 'type': 'string', + 'required': False, + 'default': 'abc@efg.com' + }, + } + } + } + + +Two notations: ``{contact: {email: 'an email'}}`` and ``{contact.email: 'an +email'}`` can be used to update the ``email`` field in the ``contact`` subdocument. + + .. _cache_control: Resource-level Cache Control @@ -2282,37 +2319,6 @@ The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline wi The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. -# Special Note on PATCH -~~~~~~~~~~~ -``PATCH`` **cannot** remove a field but only update value of the field. - -Consider the following schema: - -``` -'entity': { - 'name': { - 'type': 'string', - 'required': True }, - 'contact': { - 'type': 'dict', - 'required': True, - 'schema': { - 'phone': { - 'type': 'string', - 'required': False, - 'default': '1234567890' }, - 'email': { - 'type': 'string', - 'required': False, - 'default': 'abc@efg.com' }, - } - } -} -``` - -Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. - - Limitations ~~~~~~~~~~~ ``HATEOAS`` is not available at aggregation endpoints. This should not From 0d7c6aa12334a3dbedfc2138763f06511ee3f036 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Apr 2019 11:14:55 +0200 Subject: [PATCH 470/821] Changelog for #1234 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 662803d52..aa38ecc7f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,7 @@ New Fixed ~~~~~ +- PATCH incorrectly normalizes default values in subdocuments (`#1234`_) - Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_, `#1251`_) - Embedded documents not being sorted correctly (`#1217`_) - Eve crashes on malformed sort parameters (`#1248`_) @@ -56,6 +57,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1251`: https://github.com/pyeve/eve/pull/1251 .. _`#1245`: https://github.com/pyeve/eve/pull/1245 .. _`#1217`: https://github.com/pyeve/eve/pull/1217 From 3e1f9c17d62d06a195437982906b325e1f23bd80 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 4 Apr 2019 15:13:03 +0200 Subject: [PATCH 471/821] Fix annoying CHANGELOG formatting issues --- CHANGES.rst | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index aa38ecc7f..b785c2d0c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,8 @@ Version 0.8.2 Breaking changes ~~~~~~~~~~~~~~~~ - Werkzeug v0.15.1+ is required. You want to upgrade, otherwise your Eve - environment is likely to break. For the full story, see `#1245`_ and `#1251`_. + environment is likely to break. For the full story, see `#1245`_ and + `#1251`_. New ~~~ @@ -27,7 +28,8 @@ Fixed - Insertion failure when replacing a same document containing dbref (`#1216`_) - Datasource projection is not respected for POST requests (`#1189`_) - Soft delete removes ``auth_field`` from document (`#1188`_) -- On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT (`#1243`_) +- On Mongo 3.6+, we don't return 400 'immutable field' on PATCH and PUT + (`#1243`_) - Expecting JSON response for rate limit exceeded scenario (`#1227`_) - Multiple concurrent patches to the same record, from different processes, should result in at least one patch failing with a 412 error (Precondition @@ -39,14 +41,16 @@ Fixed - Warning: Unexpected keys present on black: ``python_version`` (`#1244`_) - UserWarning: JSON setting is deprecated. Use RENDERERS instead (`#1241`_). - DeprecationWarning: decodestring is deprecated, use decodebytes (`#1242`_) -- DeprecationWarning: count is deprecated. Use Collection.count_documents instead (`#1202`_) +- DeprecationWarning: count is deprecated. Use Collection.count_documents + instead (`#1202`_) - Documentation typos (`#1218`_, `#1240`_) Improved ~~~~~~~~ - Bump Werkzeug version to v0.15.1+ (`#1245`_, `#1251`_) - Bump PyMongo version to v3.7+ (`#1202`_) -- Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) +- Option to omit the aggregation stage when its parameter is empty/unset + (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary inside, and each key-value pair yields the related links for a data relation field (`#1204`_) @@ -104,12 +108,16 @@ New Fixed ~~~~~ -- ``mongo_indexes``: "OperationFailure" when changing the keys of an existing index (`#1180`_) +- ``mongo_indexes``: "OperationFailure" when changing the keys of an existing + index (`#1180`_) - v0.8: "OperationFailure" performing MongoDB full text searches (`#1176`_) -- "AttributeError" on Python 2.7 when obsolete ``JSON`` or ``XML`` settings are used (`#1175`_). -- "TypeError argument of type 'NoneType' is not iterable" error when using document embedding in conjuction with soft deletes (`#1120`_) +- "AttributeError" on Python 2.7 when obsolete ``JSON`` or ``XML`` settings + are used (`#1175`_). +- "TypeError argument of type 'NoneType' is not iterable" error when using + document embedding in conjuction with soft deletes (`#1120`_) - ``allow_unknown`` validation rule fails with nested dict fields (`#1163`_) -- Updating a field with a nullable data relation fails when value is null (`#1159`_) +- Updating a field with a nullable data relation fails when value is null + (`#1159`_) - "cerberus.schema.SchemaError" when ``VALIDATE_FILTERS = True``. (`#1154`_) - Serializers fails when array of types is in schema. (`#1112`_) - Replace the broken ``make audit`` shortcut with ``make check``, add the @@ -130,7 +138,7 @@ Docs ~~~~ - Typos (`#1183`_, `#1184`_, `#1185`_) - Add ``MONGO_AUTH_SOURCE`` to Quickstart. (`#1168`_) -- Fix Sphinx-embedly error when embedding speakerdeck.com slide deck. (`#1158`_) +- Fix Sphinx-embedly error when embedding speakerdeck.com slide deck (`#1158`_) - Fix broken link to the Postman app. (`#1150`_) - Update obsolete PyPI link in docs sidebar. (`#1152`_) - Only display the version number on the docs homepage. (`#1151`_) @@ -354,7 +362,8 @@ Version 0.7.10 Released on July 15, 2018. -- Fix: Pin Flask-PyMongo dependency to avoid crash with Flask-PyMongo 2. Closes #1172. +- Fix: Pin Flask-PyMongo dependency to avoid crash with Flask-PyMongo 2. + Closes #1172. Version 0.7.9 ~~~~~~~~~~~~~ @@ -569,8 +578,9 @@ Released on 6 February, 2017 - Fix: fix intermittently failing test. Closes #934 (Conrad Burchert). -- Fix: Multiple, fast (within a 1 second window) and neutral (no actual changes) - PATCH requests should not raise ``412 Precondition Failed``. Closes #920. +- Fix: Multiple, fast (within a 1 second window) and neutral (no actual + changes) PATCH requests should not raise ``412 Precondition Failed``. + Closes #920. - Fix: Resource titles are not properly escaped during the XML rendering of the root document (Kris Lambrechts). @@ -1048,8 +1058,9 @@ Released on 12 Jan, 2015. - Change: HATEOAS links are now relative to the API root. Closes #398 #401. - Change: If-Modified-Since has been disabled on resource (collections) endpoints. Same functionality is available with a ``?where={"_udpated": - {"$gt": ""}}`` request. The OpLog also allows retrieving detailed - changes happened at any endpoint, deleted documents included. Closes #334. + {"$gt": ""}}`` request. The OpLog also allows retrieving + detailed changes happened at any endpoint, deleted documents included. + Closes #334. - Change: etags are now persisted with the documents. This ensures that etags are consistent across queries, even when projection queries are issued. Please note that etags will only be stored along with new documents created From d0ab29a488f8d2703dadc77ed90b8b39b3a3c80e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 5 Apr 2019 09:07:56 +0200 Subject: [PATCH 472/821] max_results=1 should be honored on aggregation endpoints Closes #1250 --- CHANGES.rst | 2 ++ eve/io/mongo/mongo.py | 2 +- eve/methods/get.py | 2 +- eve/tests/methods/get.py | 8 ++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b785c2d0c..f009a8607 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,7 @@ New Fixed ~~~~~ +- ``max_results=1`` should be honored on aggregation endpoints (`#1250`_) - PATCH incorrectly normalizes default values in subdocuments (`#1234`_) - Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_, `#1251`_) - Embedded documents not being sorted correctly (`#1217`_) @@ -61,6 +62,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1250`: https://github.com/pyeve/eve/issues/1250 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1251`: https://github.com/pyeve/eve/pull/1251 .. _`#1245`: https://github.com/pyeve/eve/pull/1245 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 5361fc0c4..b711e9b95 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -813,7 +813,7 @@ def try_cast(v): try: r = ObjectId(unicode(v)) except NameError: - # We're on Python 3 so it's all unicode # already. + # We're on Python 3 so it's all unicode already. r = ObjectId(v) return r except: diff --git a/eve/methods/get.py b/eve/methods/get.py index 4a6316319..57dc94424 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -196,7 +196,7 @@ def prune_aggregation_stage(d): if len(stage.keys()) > 0: req_pipeline_pruned.append(stage) - if req.max_results > 1: + if req.max_results > 0: limit = {"$limit": req.max_results} skip = {"$skip": (req.page - 1) * req.max_results} req_pipeline_pruned.append(skip) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index fd8601a2b..2b076a2e8 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1381,6 +1381,14 @@ def assertOutput(doc, count, id): response, status = self.get('aggregate_test?aggregate={"$unknown":1}') self.assert200(status) + # max_results is considered + response, status = self.get( + 'aggregate_test?aggregate={"$field1":1}&max_results=1' + ) + self.assert200(status) + docs = response["_items"] + self.assertEqual(len(docs), 1) + def test_get_aggregation_parsing(self): date = datetime.utcnow() From 349d6198047789fbe961a935ef868faf0a137fd8 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Thu, 4 Apr 2019 14:45:39 -0500 Subject: [PATCH 473/821] make sure dbref encoded correctly --- eve/io/mongo/mongo.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b711e9b95..7d489fb84 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -24,6 +24,7 @@ from werkzeug.exceptions import HTTPException import decimal from bson import decimal128 +from collections import OrderedDict from eve.auth import resource_auth from eve.io.base import DataLayer, ConnectionException, BaseJSONEncoder @@ -41,6 +42,9 @@ class MongoJSONEncoder(BaseJSONEncoder): """ Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. + .. versionchanged:: 0.8.2 + Key-value pair order in DBRef are honored when encoding. Closes #1255. + .. versionchanged:: 0.6.2 Do not attempt to serialize callables. Closes #790. @@ -57,7 +61,9 @@ def default(self, obj): # (and we probably don't want it to be exposed anyway). See #790. return "" if isinstance(obj, DBRef): - retval = {"$id": str(obj.id), "$ref": obj.collection} + retval = OrderedDict() + retval["$ref"] = obj.collection + retval["$id"] = str(obj.id) if obj.database: retval["$db"] = obj.database return retval From 68b8d38d61380ac9a413309095999b70219fbce1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 5 Apr 2019 09:13:08 +0200 Subject: [PATCH 474/821] Changelog for #1256 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f009a8607..7d68523b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,7 @@ New Fixed ~~~~~ +- Insertion failure when replacing unknown field with dbref value (`#1255`_) - ``max_results=1`` should be honored on aggregation endpoints (`#1250`_) - PATCH incorrectly normalizes default values in subdocuments (`#1234`_) - Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_, `#1251`_) @@ -62,6 +63,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1255`: https://github.com/pyeve/eve/issues/1255 .. _`#1250`: https://github.com/pyeve/eve/issues/1250 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1251`: https://github.com/pyeve/eve/pull/1251 From 0c65010b1d3827751d6d45a45124e0a0934567b6 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 5 Apr 2019 17:49:09 -0500 Subject: [PATCH 475/821] fix dbref encoding if json_sort_keys is true --- eve/io/mongo/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 7d489fb84..7f59cc79e 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -66,7 +66,7 @@ def default(self, obj): retval["$id"] = str(obj.id) if obj.database: retval["$db"] = obj.database - return retval + return json.RawJSON(json.dumps(retval)) if isinstance(obj, decimal128.Decimal128): return str(obj) # delegate rendering to base class method From 65b06fd753272c6b2fc8b190da76eebc739b267e Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 5 Apr 2019 20:39:49 -0500 Subject: [PATCH 476/821] add HTTP status code to handle malformed DBRef --- eve/io/mongo/mongo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 7f59cc79e..70835f622 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -495,6 +495,9 @@ def insert(self, resource, doc_or_docs): def _change_request(self, resource, id_, changes, original, replace=False): """ Performs a change, be it a replace or update. + .. versionchanged:: 0.8.2 + Return 400 if update/replace with malformed DBRef field. See #1257. + .. versionchanged:: 0.6.1 Support for PyMongo 3.0. @@ -530,6 +533,13 @@ def _change_request(self, resource, id_, changes, original, replace=False): "pymongo.errors.DuplicateKeyError: %s" % e ), ) + except pymongo.errors.WriteError as e: + abort( + 400, + description=debug_error_message( + "pymongo.errors.WriteError: %s" % e + ), + ) except pymongo.errors.OperationFailure as e: # server error codes and messages changed between 2.4 and 2.6/3.0. server_version = self.driver.db.client.server_info()["version"][:3] From bae88e2f5711faeffa74dcd5b6c418a7af4aba9d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 8 Apr 2019 10:17:52 +0200 Subject: [PATCH 477/821] black formatting magic --- eve/io/mongo/mongo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 70835f622..bee6559b2 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -536,9 +536,7 @@ def _change_request(self, resource, id_, changes, original, replace=False): except pymongo.errors.WriteError as e: abort( 400, - description=debug_error_message( - "pymongo.errors.WriteError: %s" % e - ), + description=debug_error_message("pymongo.errors.WriteError: %s" % e), ) except pymongo.errors.OperationFailure as e: # server error codes and messages changed between 2.4 and 2.6/3.0. From 866d5e156afe1e0d661d24e880e9d150feb99bd5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 8 Apr 2019 10:20:53 +0200 Subject: [PATCH 478/821] Changelog for #1257 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7d68523b3..5d35f48e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,7 +21,8 @@ New Fixed ~~~~~ -- Insertion failure when replacing unknown field with dbref value (`#1255`_) +- Insertion failure when replacing unknown field with dbref value (`#1255`_, + `#1257`_) - ``max_results=1`` should be honored on aggregation endpoints (`#1250`_) - PATCH incorrectly normalizes default values in subdocuments (`#1234`_) - Unauthorized Exception not working with Werkzeug >= 15.0 (`#1245`_, `#1251`_) @@ -63,6 +64,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1257`: https://github.com/pyeve/eve/issues/1257 .. _`#1255`: https://github.com/pyeve/eve/issues/1255 .. _`#1250`: https://github.com/pyeve/eve/issues/1250 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 From d81bed84b815be011b66391c0d9c2c9694457030 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 8 Apr 2019 10:41:42 +0200 Subject: [PATCH 479/821] Fix for test failure introduced with #1257 --- eve/io/mongo/mongo.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index bee6559b2..1f79eb4e6 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -533,12 +533,7 @@ def _change_request(self, resource, id_, changes, original, replace=False): "pymongo.errors.DuplicateKeyError: %s" % e ), ) - except pymongo.errors.WriteError as e: - abort( - 400, - description=debug_error_message("pymongo.errors.WriteError: %s" % e), - ) - except pymongo.errors.OperationFailure as e: + except (pymongo.errors.WriteError, pymongo.errors.OperationFailure) as e: # server error codes and messages changed between 2.4 and 2.6/3.0. server_version = self.driver.db.client.server_info()["version"][:3] if (server_version == "2.4" and e.code in (13596, 10148)) or ( From 10c60f60f6787ecdbd0b913924e844049fd9f2e9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 9 Apr 2019 10:24:23 +0200 Subject: [PATCH 480/821] Use an OrderDict for project_urls --- setup.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 2d1a1517e..30645a03a 100755 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import re from setuptools import setup, find_packages +from collections import OrderedDict DESCRIPTION = "Python REST API for Humans." with open("README.rst") as f: @@ -34,11 +35,13 @@ author="Nicola Iarocci", author_email="eve@nicolaiarocci.com", url="http://python-eve.org", - project_urls={ - "Documentation": "http://python-eve.org", - "Code": "https://github.com/pyeve/eve", - "Issue tracker": "https://github.com/pyeve/eve/issues", - }, + project_urls=OrderedDict( + ( + ("Documentation", "http://python-eve.org"), + ("Code", "https://github.com/pyeve/eve"), + ("Issue tracker", "https://github.com/pyeve/eve/issues"), + ) + ), license="BSD", platforms=["any"], packages=find_packages(), From 71f80f76af2c8c417e9256ba7a72b659a505bdd7 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sun, 11 Nov 2018 17:36:39 +1000 Subject: [PATCH 481/821] added facet to _perform_aggregation --- eve/methods/get.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 57dc94424..903f45432 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -189,33 +189,44 @@ def prune_aggregation_stage(d): for stage in req_pipeline: parse_aggregation_stage(stage, key, value) - # remove the stages whose conditions are not yet set - req_pipeline_pruned = [] - for stage in req_pipeline: - prune_aggregation_stage(stage) - if len(stage.keys()) > 0: - req_pipeline_pruned.append(stage) - - if req.max_results > 0: + paginated_results = [] + if req.max_results > 1: limit = {"$limit": req.max_results} skip = {"$skip": (req.page - 1) * req.max_results} - req_pipeline_pruned.append(skip) - req_pipeline_pruned.append(limit) + paginated_results.append(skip) + paginated_results.append(limit) + else: + # sub-pipeline in $facet stage cannot be empty + skip = {"$skip": 0} + paginated_results.append(skip) + + facet_pipelines = {} + facet_pipelines["paginated_results"] = paginated_results + facet_pipelines["total_count"] = [{"$count": "count"}] + + facet = {"$facet": facet_pipelines} + + req_pipeline.append(facet) getattr(app, "before_aggregation")(resource, req_pipeline_pruned) - cursor = app.data.aggregate(resource, req_pipeline_pruned, options) + cursor = app.data.aggregate(resource, req_pipeline, options).next() - for document in cursor: + for document in cursor["paginated_results"]: documents.append(document) getattr(app, "after_aggregation")(resource, documents) response[config.ITEMS] = documents - # PyMongo's CommandCursor does not return a count, so we cannot - # provide pagination/total count info as we do with a normal - # (non-aggregate) GET request. + count = cursor["total_count"][0]["count"] + + # add pagination info + if config.DOMAIN[resource]["pagination"]: + response[config.META] = _meta_links(req, count) + + if config.DOMAIN[resource]["hateoas"]: + response[config.LINKS] = _pagination_links(resource, req, count) return response, None, None, 200, [] From b641542c4911da879445d7dadaa97e08b4aff4c6 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Mon, 12 Nov 2018 00:14:29 +1000 Subject: [PATCH 482/821] list index out of range --- eve/methods/get.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 903f45432..0d6720228 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -219,7 +219,11 @@ def prune_aggregation_stage(d): response[config.ITEMS] = documents - count = cursor["total_count"][0]["count"] + if cursor["total_count"]: + # IndexError: list index out of range + count = cursor["total_count"][0]["count"] + else: + count = 0 # add pagination info if config.DOMAIN[resource]["pagination"]: From fc2d74dcdadd8d68ad038697fdc6ae1feda03058 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 6 Apr 2019 14:22:22 +1000 Subject: [PATCH 483/821] Append facet after before_aggregation hook to allow pipline modification --- eve/methods/get.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 0d6720228..f88912f5b 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -190,6 +190,7 @@ def prune_aggregation_stage(d): parse_aggregation_stage(stage, key, value) paginated_results = [] + if req.max_results > 1: limit = {"$limit": req.max_results} skip = {"$skip": (req.page - 1) * req.max_results} @@ -206,9 +207,12 @@ def prune_aggregation_stage(d): facet = {"$facet": facet_pipelines} - req_pipeline.append(facet) + getattr(app, "before_aggregation")(resource, req_pipeline) - getattr(app, "before_aggregation")(resource, req_pipeline_pruned) + # Appending $facet afer the before_aggregation hook allows for + # easy modification of the orginal pipline, however, pagination + # (skip, limit) cannot be accessed. + req_pipeline.append(facet) cursor = app.data.aggregate(resource, req_pipeline, options).next() From 849a535003e9b59067e04cc180485307e23d4133 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 6 Apr 2019 14:23:22 +1000 Subject: [PATCH 484/821] Added tests for _links and pagination into test_get_aggregation_pagination --- eve/tests/methods/get.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2b076a2e8..4e4b2a1f0 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1556,6 +1556,11 @@ def test_get_aggregation_pagination(self): response, status = self.get("aggregate_test") self.assert200(status) + links = response["_links"] + self.assertNextLink(links, 2) + self.assertLastLink(links, 3) + self.assertPagination(response, 1, 75, 25) + items = response["_items"] expected_length = self.app.config["PAGINATION_DEFAULT"] self.assertEqual(len(items), expected_length) @@ -1569,6 +1574,12 @@ def test_get_aggregation_pagination(self): response, status = self.get("aggregate_test?page=2") self.assert200(status) + links = response["_links"] + self.assertNextLink(links, 3) + self.assertPrevLink(links, 1) + self.assertLastLink(links, 3) + self.assertPagination(response, 2, 75, 25) + items = response["_items"] expected_length = self.app.config["PAGINATION_DEFAULT"] self.assertEqual(len(items), expected_length) @@ -1582,6 +1593,11 @@ def test_get_aggregation_pagination(self): response, status = self.get("aggregate_test?page=3") self.assert200(status) + links = response["_links"] + self.assertPrevLink(links, 2) + self.assertLastLink(links, None) + self.assertPagination(response, 3, 75, 25) + items = response["_items"] expected_length = num - self.app.config["PAGINATION_DEFAULT"] * 2 self.assertEqual(len(items), expected_length) From 2a1413e4ff8e40b66305ea2654cae2cd4f2bccd2 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Sat, 6 Apr 2019 14:45:03 +1000 Subject: [PATCH 485/821] Resolved conflicts between master --- eve/methods/get.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index f88912f5b..83bdc1a35 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -189,9 +189,16 @@ def prune_aggregation_stage(d): for stage in req_pipeline: parse_aggregation_stage(stage, key, value) + # remove the stages whose conditions are not yet set + req_pipeline_pruned = [] + for stage in req_pipeline: + prune_aggregation_stage(stage) + if len(stage.keys()) > 0: + req_pipeline_pruned.append(stage) + paginated_results = [] - if req.max_results > 1: + if req.max_results > 0: limit = {"$limit": req.max_results} skip = {"$skip": (req.page - 1) * req.max_results} paginated_results.append(skip) @@ -207,14 +214,14 @@ def prune_aggregation_stage(d): facet = {"$facet": facet_pipelines} - getattr(app, "before_aggregation")(resource, req_pipeline) + getattr(app, "before_aggregation")(resource, req_pipeline_pruned) # Appending $facet afer the before_aggregation hook allows for # easy modification of the orginal pipline, however, pagination # (skip, limit) cannot be accessed. - req_pipeline.append(facet) + req_pipeline_pruned.append(facet) - cursor = app.data.aggregate(resource, req_pipeline, options).next() + cursor = app.data.aggregate(resource, req_pipeline_pruned, options).next() for document in cursor["paginated_results"]: documents.append(document) From 76b170b55c2b6b6283b83e6c21e21a0fc63a6b00 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Tue, 9 Apr 2019 17:54:57 +1000 Subject: [PATCH 486/821] removed HATEOAS from aggregation limitations --- docs/features.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 88bf9e946..53efb2de9 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2321,10 +2321,6 @@ Custom callback functions can be attached to the ``before_aggregation`` and ``af Limitations ~~~~~~~~~~~ -``HATEOAS`` is not available at aggregation endpoints. This should not -be surprising as documents returned by these endpoints are aggregation results -and do not reside on the database, so there is no static link available for them. - Client pagination (``?page=2``) is enabled by default. This is currently achieved by injecting two additional stages (``$limit`` first, then ``$skip``) to the very end of the aggregation pipeline. You can turn pagination off by setting From d49113c665211d48e3301020766c47dc00e7da12 Mon Sep 17 00:00:00 2001 From: Wytamma Wirth Date: Tue, 9 Apr 2019 18:32:07 +1000 Subject: [PATCH 487/821] Aggregation pagination explination includes facet --- docs/features.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 53efb2de9..e1b645cb7 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2322,9 +2322,10 @@ Custom callback functions can be attached to the ``before_aggregation`` and ``af Limitations ~~~~~~~~~~~ Client pagination (``?page=2``) is enabled by default. This is currently -achieved by injecting two additional stages (``$limit`` first, then ``$skip``) -to the very end of the aggregation pipeline. You can turn pagination off by setting -``pagination`` to ``False`` for the endpoint. Keep in mind that, when pagination +achieved by injecting a ``$facet`` stage contianing two sub-pipelines, +total_count (``$count``) and paginated_results (``$limit`` first, then ``$skip``) +to the very end of the aggregation pipeline after the ``before_aggregation`` hook. +You can turn pagination off by setting ``pagination`` to ``False`` for the endpoint. Keep in mind that, when pagination is disabled, all aggregation results are included with every response. Disabling pagination might be appropriate (and actually advisable) only if the expected response payload is not huge. From f1698ba43ca93f4afb267656b522abe9ea7a1230 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 10 Apr 2019 10:40:42 +0200 Subject: [PATCH 488/821] Changelog for #1258 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5d35f48e9..423a8c4e0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ Breaking changes New ~~~ +- HATEOAS support added to aggregation results (`#1208`_) - ``on_fetched_diffs`` event hooks (`#1224`_) - Python 3.7 added to the CI matrix (`#1199`_) - Support for Mongo 3.6+ ``$expr`` query operator. @@ -64,6 +65,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1208`: https://github.com/pyeve/eve/issues/1208 .. _`#1257`: https://github.com/pyeve/eve/issues/1257 .. _`#1255`: https://github.com/pyeve/eve/issues/1255 .. _`#1250`: https://github.com/pyeve/eve/issues/1250 From d5e79ccf130e607283d17a95e25bd5a1538d2fc1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 10 Apr 2019 10:41:23 +0200 Subject: [PATCH 489/821] Wytamma Wirth --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 874c361e6..ec3c34192 100644 --- a/AUTHORS +++ b/AUTHORS @@ -179,6 +179,7 @@ Patches and Contributions - Wael M. Nasreddine - Wan Bachtiar - Wei Guan +- Wytamma Wirth - Xavi Cubillas - boosh - dccrazyboy From 094b02064bebcdade30118a2d5607c817529d90d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 10 Apr 2019 11:00:24 +0200 Subject: [PATCH 490/821] Bump version to 0.9-dev0 --- CHANGES.rst | 4 ++-- eve/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 423a8c4e0..8e0d4e3c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,8 +3,8 @@ Eve Changelog Here you can see the full list of changes between each Eve release. -Version 0.8.2 -------------- +Version 0.9 +----------- Breaking changes ~~~~~~~~~~~~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index 9f7ca9ca6..8b5a89c64 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.8.2.dev0" +__version__ = "0.9-dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From cb083627f44c3c67fe15518a00f80d2487c2bafb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 10 Apr 2019 11:01:55 +0200 Subject: [PATCH 491/821] Move py37 support to 'improved' section of changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8e0d4e3c6..df4c1ed9d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,6 @@ New ~~~ - HATEOAS support added to aggregation results (`#1208`_) - ``on_fetched_diffs`` event hooks (`#1224`_) -- Python 3.7 added to the CI matrix (`#1199`_) - Support for Mongo 3.6+ ``$expr`` query operator. - Support for Mongo 3.6+ ``$center`` query operator. @@ -53,6 +52,7 @@ Improved ~~~~~~~~ - Bump Werkzeug version to v0.15.1+ (`#1245`_, `#1251`_) - Bump PyMongo version to v3.7+ (`#1202`_) +- Python 3.7 added to the CI matrix (`#1199`_) - Option to omit the aggregation stage when its parameter is empty/unset (`#1209`_) - HATEOAS: now the ``_links`` dictionary may have a ``related`` dictionary From a885efc54acbb9dfe9e6c060b4dff531b3814754 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 10 Apr 2019 11:16:54 +0200 Subject: [PATCH 492/821] Remove 'you are looking at dev docs' warning As we're running on readthedocs.org now, which provides a convenient way to switch between dev and prod docs. --- docs/_templates/sidebarintro.html | 30 ++++++++++++++++-------------- docs/index.rst | 6 ------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 0c92c5780..3049fb1b7 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,10 +1,13 @@

    Stay Informed

    Receive updates on new releases and upcoming projects.

    -

    +

    + +

    -

    +

    Join Mailing List.

    @@ -12,7 +15,8 @@

    Eve Course

    This course will teach you how to build RESTful services with Eve and MongoDB.

    The teacher is the project creator and maintainer.

    Useful Links

    @@ -30,14 +34,12 @@

    Other Projects

    More Nicola Iarocci projects:

    - -

    You are looking at the documentation of the development version.

    diff --git a/docs/index.rst b/docs/index.rst index 02924d687..bba741ddb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,12 +81,6 @@ show you how easy it is to run an API with Eve. You will also find `usage examples`_ for all common use cases (GET, POST, PATCH, DELETE and more). There is also a simple `client app`_ available. -Development Version --------------------- -If you are on python-eve.org_ then you are looking at the documentation of the -development version. Looking for last release docs? Follow `this -link `_. - .. toctree:: :hidden: From b56ebf9aa8872c2415e4b59fd683e02a9dd9c439 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Apr 2019 10:14:52 +0200 Subject: [PATCH 493/821] normalize version number --- docs/index.rst | 6 +++--- eve/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index bba741ddb..e45c4ef6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,9 +29,9 @@ Eve is an :doc:`open source ` Python REST API framework designed for human beings. It allows to effortlessly build and deploy highly customizable, fully featured RESTful Web Services. -Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data -stores. Support for SQL, Elasticsearch and Neo4js backends is provided by -community extensions_. +Eve is powered by Flask_ and Cerberus_ and it offers native support for +MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is +provided by community extensions_. The codebase is thoroughly tested under Python 2.7, 3.4+, and PyPy. diff --git a/eve/__init__.py b/eve/__init__.py index 8b5a89c64..1d13e3454 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9-dev0" +__version__ = "0.9.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 255a02c3a80bc4ca76c5dfde9f95503f69c69930 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Apr 2019 10:20:15 +0200 Subject: [PATCH 494/821] Package should be distributed as a python wheel Closes #1260 --- CHANGES.rst | 2 ++ Makefile | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index df4c1ed9d..022525083 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -50,6 +50,7 @@ Fixed Improved ~~~~~~~~ +- Eve package is now distributed as a Python wheel (`#1260`_) - Bump Werkzeug version to v0.15.1+ (`#1245`_, `#1251`_) - Bump PyMongo version to v3.7+ (`#1202`_) - Python 3.7 added to the CI matrix (`#1199`_) @@ -65,6 +66,7 @@ Improved - Add a "Python 3 is highly preferred" note on the homepage (`#1198`_) - Drop sphinx-contrib-embedly when building docs. +.. _`#1260`: https://github.com/pyeve/eve/issues/1260 .. _`#1208`: https://github.com/pyeve/eve/issues/1208 .. _`#1257`: https://github.com/pyeve/eve/issues/1257 .. _`#1255`: https://github.com/pyeve/eve/issues/1255 diff --git a/Makefile b/Makefile index 8578316d9..92a4a3854 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload +.PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload wheel install-dev: pip install -q -e .[dev] @@ -11,6 +11,9 @@ test-all: clean-pyc install-dev tox: test-all +wheel: + python setup.py sdist bdist_wheel --universal + BUILDDIR = _build docs: install-dev $(MAKE) -C docs html BUILDDIR=$(BUILDDIR) From 6e5d5f1f9e81e2ab898713f693c5072f4bc6c53e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Apr 2019 10:34:34 +0200 Subject: [PATCH 495/821] drop redundant --universal option --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 92a4a3854..3cdff7f8b 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ test-all: clean-pyc install-dev tox: test-all wheel: - python setup.py sdist bdist_wheel --universal + python setup.py sdist bdist_wheel BUILDDIR = _build docs: install-dev From 5e55bff1ef375feeaf7ee0a164ffd9a7f27487ab Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Apr 2019 10:42:02 +0200 Subject: [PATCH 496/821] add long_description_content_type attribute to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 30645a03a..bff7a75fd 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, + long_description_content_type="text/x-rst", author="Nicola Iarocci", author_email="eve@nicolaiarocci.com", url="http://python-eve.org", From 4fcb087873c6ace250ef7060621d0decc558433f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Apr 2019 10:53:55 +0200 Subject: [PATCH 497/821] Bump version to 0.9 --- CHANGES.rst | 2 ++ eve/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 022525083..b78a0e977 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ Here you can see the full list of changes between each Eve release. Version 0.9 ----------- +Released on April 11, 2019. + Breaking changes ~~~~~~~~~~~~~~~~ - Werkzeug v0.15.1+ is required. You want to upgrade, otherwise your Eve diff --git a/eve/__init__.py b/eve/__init__.py index 1d13e3454..6c1888121 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9.dev0" +__version__ = "0.9" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 958b4537038f40e0a436b58f3ce3a6f600ce2039 Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Fri, 26 Apr 2019 10:14:29 +0200 Subject: [PATCH 498/821] Fix crash when trying to ignore a nested field that is not present in the object to hash --- eve/tests/utils.py | 10 ++++++++++ eve/utils.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/eve/tests/utils.py b/eve/tests/utils.py index 5d98b256c..6cf2e6793 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -197,6 +197,16 @@ def test_document_etag_ignore_fields(self): hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) ) + # ignore fiels nested using doting notation when a root part of the field is not present + test = {"key1": "value1", "dict": {"key2": "value2"}} + ignore_fields = ["dict2.key3"] + test_without_ignore = {"key1": "value1", "dict": {"key2": "value2"}} + challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") + with self.app.test_request_context(): + self.assertEqual( + hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) + ) + def test_extract_key_values(self): test = { "key1": "value1", diff --git a/eve/utils.py b/eve/utils.py index 8db19ac3c..db430c8ab 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -344,7 +344,7 @@ def filter_ignore_fields(d, fields): # to nested keys such as ["foo", "dict.bar", "dict.joe"] for field in fields: key, _, value = field.partition(".") - if value: + if value and key in d: filter_ignore_fields(d[key], [value]) elif field in d: d.pop(field) From a09ecf1ce8563d83aac8d316b656ee908206992c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 29 Apr 2019 09:37:30 +0200 Subject: [PATCH 499/821] Changelog for #1263 --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b78a0e977..cc80e8bf2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,13 @@ Eve Changelog Here you can see the full list of changes between each Eve release. +In Development +--------------- + +- Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) + +.. _`#1263`: https://github.com/pyeve/eve/pull/1263 + Version 0.9 ----------- From 8bdc88591b56bf38ff0051795d46285de471c2ec Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 2 May 2019 10:46:18 +0200 Subject: [PATCH 500/821] Remove transparent_schema_rules from docs Addresses #1264 --- CHANGES.rst | 8 ++++++++ docs/config.rst | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cc80e8bf2..897794659 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,10 +6,18 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +Fixed +~~~~~ - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) .. _`#1263`: https://github.com/pyeve/eve/pull/1263 +Improved +~~~~~~~~ +- Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) + +.. _`#1264`: https://github.com/pyeve/eve/issues/1264 + Version 0.9 ----------- diff --git a/docs/config.rst b/docs/config.rst index 34ae85433..c2ac2bfe7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -978,9 +978,6 @@ always lowercase. ``ALLOW_UNKNOWN``. See :ref:`unknown` for more information. Defaults to ``False``. -``transparent_schema_rules`` When ``True``, this option disables - :ref:`schema_validation` for the endpoint. - ``projection`` When ``True``, this option enables the :ref:`projections` feature. Locally overrides ``PROJECTION``. Defaults to ``True``. From 2757e81351206196b6ecabd8001798963ccf5afc Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Mon, 22 Apr 2019 14:49:03 -0700 Subject: [PATCH 501/821] Add field `normalize_document_for_patch` to control whether apply normalize for validator of patch request. By default it is true, in this case the fields which are not included in the patch body will be reset to the default value specified in the schema. --- docs/config.rst | 10 ++++++++++ docs/features.rst | 31 +++++++++++++++++++++++++++++++ eve/default_settings.py | 5 +++++ eve/flaskapp.py | 3 +++ eve/methods/patch.py | 5 ++++- eve/tests/test_settings.py | 1 + eve/validation.py | 9 +++++++-- 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index c2ac2bfe7..7fda0e4cd 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -776,6 +776,11 @@ uppercase. note that with the default Mongo layer, setting this to ``False`` will result in an error. Defaults to ``True``. +``NORMALIZE_DOCUMENT_FOR_PATCH`` If ``True``, the patch document will be + normalized according to schema. This means + if a field is not included in the patch + body, it will be reset to the default value + in its schema. =================================== ========================================= @@ -1117,6 +1122,11 @@ always lowercase. with the default Mongo layer, setting this to ``False`` will result in an error. Defaults to ``True``. +``normalize_document_for_patch`` If ``True``, the patch document will be + normalized according to schema. This means + if a field is not included in the patch + body, it will be reset to the default value + in its schema. =============================== =============================================== diff --git a/docs/features.rst b/docs/features.rst index e1b645cb7..2204f143a 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2319,6 +2319,37 @@ The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline wi The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. +# Special Note on PATCH +~~~~~~~~~~~ +``PATCH`` **cannot** remove a field but only update value of the field. + +Consider the following schema: + +``` +'entity': { + 'name': { + 'type': 'string', + 'required': True }, + 'contact': { + 'type': 'dict', + 'required': True, + 'schema': { + 'phone': { + 'type': 'string', + 'required': False, + 'default': '1234567890' }, + 'email': { + 'type': 'string', + 'required': False, + 'default': 'abc@efg.com' }, + } + } +} +``` + +Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. + + Limitations ~~~~~~~~~~~ Client pagination (``?page=2``) is enabled by default. This is currently diff --git a/eve/default_settings.py b/eve/default_settings.py index 38dc36e2f..e1ad92496 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -265,3 +265,8 @@ # aknowledged writes). This is also the current PyMongo/Mongo default setting. MONGO_WRITE_CONCERN = {"w": 1} MONGO_OPTIONS = {"connect": True, "tz_aware": True} + +# if true, the document will be normalized according to the schema during patch +# this means the fields will be reset to the default value, if not contained in +# the patch body. +NORMALIZE_DOCUMENT_FOR_PATCH = True diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 8fb953fee..d9998ce76 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -681,6 +681,9 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault( "normalize_dotted_fields", self.config["NORMALIZE_DOTTED_FIELDS"] ) + settings.setdefault( + "normalize_document_for_patch", self.config["NORMALIZE_DOCUMENT_FOR_PATCH"] + ) # empty schemas are allowed for read-only access to resources schema = settings.setdefault("schema", {}) self.set_schema_defaults(schema, settings["id_field"]) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index e6270f687..3c416e71d 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -152,6 +152,7 @@ def patch_internal( resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] + normalize_document = resource_def.get("normalize_document_for_patch") validator = app.validator( schema, resource=resource, allow_unknown=resource_def["allow_unknown"] ) @@ -174,7 +175,9 @@ def patch_internal( if skip_validation: validation = True else: - validation = validator.validate_update(updates, object_id, original) + validation = validator.validate_update( + updates, object_id, original, normalize_document + ) updates = validator.document if validation: diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 3c988a843..0ba982623 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -246,6 +246,7 @@ test_patch = { "datasource": {"source": "test_patch"}, + "normalize_document_for_patch": False, "schema": { "name": {"type": "string", "required": True}, "contact": { diff --git a/eve/validation.py b/eve/validation.py index 20abae38e..811d393a1 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -27,17 +27,22 @@ def __init__(self, *args, **kwargs): super(Validator, self).__init__(*args, **kwargs) - def validate_update(self, document, document_id, persisted_document=None): + def validate_update( + self, document, document_id, persisted_document=None, normalize_document=True + ): """ Validate method to be invoked when performing an update, not an insert. :param document: the document to be validated. :param document_id: the unique id of the document. :param persisted_document: the persisted document to be updated. + :param normalize_document: whether apply normalization during patch. """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document, update=True) + return super(Validator, self).validate( + document, update=True, normalize=normalize_document + ) def validate_replace(self, document, document_id, persisted_document=None): """ Validation method to be invoked when performing a document From 7d0217b8a41ff177cf73169f634fbdc8caa62ceb Mon Sep 17 00:00:00 2001 From: Qiang Zhang Date: Sat, 4 May 2019 22:43:33 -0700 Subject: [PATCH 502/821] Update the documentation and add examples. --- docs/config.rst | 8 ++++++-- docs/features.rst | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 7fda0e4cd..f39739989 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -780,7 +780,9 @@ uppercase. normalized according to schema. This means if a field is not included in the patch body, it will be reset to the default value - in its schema. + in its schema. If ``False``, the field which + is not included in the patch body will be + kept untouched. Defaults to ``True``. =================================== ========================================= @@ -1126,7 +1128,9 @@ always lowercase. normalized according to schema. This means if a field is not included in the patch body, it will be reset to the default value - in its schema. + in its schema. If ``False``, the field which + is not included in the patch body will be + kept untouched. Defaults to ``True``. =============================== =============================================== diff --git a/docs/features.rst b/docs/features.rst index 2204f143a..649c6934a 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -2349,6 +2349,31 @@ Consider the following schema: Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. +``PATCH`` incorrectly normalizes default values in sub-documents. + +Consider the example above, by default, if you apply PATCH with body + +``` +{'contact.email': 'xyz@gmail.com'} +``` + +to the document: + +``` +{'name': 'test account', 'contact': {'email': '123@yahoo.com', 'phone': '9876543210'}} +``` + +The document will be updated as: + +``` +{'name': 'test account', 'contact': {'email': 'xyz@gmail.com', 'phone': '1234567890'}} +``` + +That is the ``contact.phone`` has been reset to the default value in the schema. To avoid this, you could set `False` to the parameter: ``normalize_document_for_patch`` (or ``NORMALIZE_DOCUMENT_FOR_PATCH`` globally), in which case, the document will be updated as: + +``` +{'name': 'test account', 'contact': {'email': '123@yahoo.com', 'phone': '9876543210'}} +``` Limitations ~~~~~~~~~~~ From 375e3aab8c1337ca384d3b627881973034dfbe90 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 5 May 2019 08:44:49 +0200 Subject: [PATCH 503/821] Skip PEP517 on make's install-dev option --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3cdff7f8b..37f4c0893 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload wheel install-dev: - pip install -q -e .[dev] + pip install -q -e .[dev] --no-use-pep517 test: clean-pyc install-dev pytest From 17861f5828f31fb30631f4b66c3503a84002cb1a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 5 May 2019 08:45:26 +0200 Subject: [PATCH 504/821] s/NORMALIZE_DOCUMENT_FOR_PATCH/NORMALIZE_ON_PATCH Also, some docs improvements for the feature. Addresses #1234 --- docs/config.rst | 16 ++--- docs/features.rst | 133 ++++++++++++++++++------------------- eve/default_settings.py | 6 +- eve/flaskapp.py | 4 +- eve/methods/patch.py | 2 +- eve/tests/test_settings.py | 2 +- 6 files changed, 79 insertions(+), 84 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index f39739989..60c4de08a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -776,7 +776,7 @@ uppercase. note that with the default Mongo layer, setting this to ``False`` will result in an error. Defaults to ``True``. -``NORMALIZE_DOCUMENT_FOR_PATCH`` If ``True``, the patch document will be +``NORMALIZE_ON_PATCH`` If ``True``, the patch document will be normalized according to schema. This means if a field is not included in the patch body, it will be reset to the default value @@ -1124,13 +1124,13 @@ always lowercase. with the default Mongo layer, setting this to ``False`` will result in an error. Defaults to ``True``. -``normalize_document_for_patch`` If ``True``, the patch document will be - normalized according to schema. This means - if a field is not included in the patch - body, it will be reset to the default value - in its schema. If ``False``, the field which - is not included in the patch body will be - kept untouched. Defaults to ``True``. +``normalze_on_patch`` If ``True``, the patch document will be + normalized according to schema. This means if + a field is not included in the patch body, it + will be reset to the default value in its + schema. If ``False``, the field which is not + included in the patch body will be kept + untouched. Defaults to ``True``. =============================== =============================================== diff --git a/docs/features.rst b/docs/features.rst index 649c6934a..965a42c89 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -768,7 +768,53 @@ Consider the following schema: Two notations: ``{contact: {email: 'an email'}}`` and ``{contact.email: 'an -email'}`` can be used to update the ``email`` field in the ``contact`` subdocument. +email'}`` can be used to update the ``email`` field in the ``contact`` +subdocument. + +Keep in mind that ``PATCH`` cannot remove a field, but only update existing +values. Also, by default ``PATCH`` will normalize missing body fields that +have defautl values defined in the schema. Consider the schema above. If your +``PATCH`` has a body like this: + +:: + + {'contact.email': 'xyz@gmail.com'} + +and targets this document: + +:: + + { + 'name': 'test account', + 'contact': {'email': '123@yahoo.com', 'phone': '9876543210'} + } + +Then the updated document will look like this: + +:: + + { + 'name': 'test account', + 'contact': { + 'email': 'xyz@gmail.com', + 'phone': '1234567890' + } + } + +That is, ``contact.phone`` has been reset to its default value. This might +now been the desired behavior. To change it, you can set +``normalize_on_patch`` (or ``NORMALIZE_ON_PATCH`` globally) to ``False``. +Now the updated document will look like this: + +:: + + { + 'name': 'test account', + 'contact': { + 'email': '123@yahoo.com', + 'phone': '9876543210' + } + } .. _cache_control: @@ -2286,7 +2332,8 @@ to a keyword of your liking, just set ``QUERY_AGGREGATION`` in your settings. You can also set all options natively supported by PyMongo. For more information on aggregation see :ref:`datasource`. -You can pass ``{}`` to fields which you want to ignore. Considering the following pipelines: +You can pass ``{}`` to fields which you want to ignore. Considering the +following pipelines: :: @@ -2308,83 +2355,33 @@ If performing the following request: $ curl -i http://example.com/posts?aggregate={"$name": {"$regex": "Apple"}, "$time": {}} -The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline will be executed as ``{"$match": { "name": {"$regex": "Apple"}}}``. And for the following request: +The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline +will be executed as ``{"$match": { "name": {"$regex": "Apple"}}}``. And for +the following request: :: $ curl -i http://example.com/posts?aggregate={"$name": {}, "$time": {}} -The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline will be completely skipped. - -The request above will ignore ``"count": {"$sum": "$value"}}``. A -Custom callback functions can be attached to the ``before_aggregation`` and ``after_aggregation`` event hooks. For more information, see :ref:`aggregation_hooks`. - -# Special Note on PATCH -~~~~~~~~~~~ -``PATCH`` **cannot** remove a field but only update value of the field. - -Consider the following schema: - -``` -'entity': { - 'name': { - 'type': 'string', - 'required': True }, - 'contact': { - 'type': 'dict', - 'required': True, - 'schema': { - 'phone': { - 'type': 'string', - 'required': False, - 'default': '1234567890' }, - 'email': { - 'type': 'string', - 'required': False, - 'default': 'abc@efg.com' }, - } - } -} -``` - -Two notations ``contact: { email: 'an email'}`` and ``contact.email: 'an email'`` can be used to update the `email` field embedded in `contact` field. - -``PATCH`` incorrectly normalizes default values in sub-documents. - -Consider the example above, by default, if you apply PATCH with body - -``` -{'contact.email': 'xyz@gmail.com'} -``` - -to the document: - -``` -{'name': 'test account', 'contact': {'email': '123@yahoo.com', 'phone': '9876543210'}} -``` - -The document will be updated as: - -``` -{'name': 'test account', 'contact': {'email': 'xyz@gmail.com', 'phone': '1234567890'}} -``` - -That is the ``contact.phone`` has been reset to the default value in the schema. To avoid this, you could set `False` to the parameter: ``normalize_document_for_patch`` (or ``NORMALIZE_DOCUMENT_FOR_PATCH`` globally), in which case, the document will be updated as: +The stage ``{"$match": { "name": "$name", "time": "$time"}}`` in the pipeline +will be completely skipped. -``` -{'name': 'test account', 'contact': {'email': '123@yahoo.com', 'phone': '9876543210'}} -``` +The request above will ignore ``"count": {"$sum": "$value"}}``. A Custom +callback functions can be attached to the ``before_aggregation`` and +``after_aggregation`` event hooks. For more information, see +:ref:`aggregation_hooks`. Limitations ~~~~~~~~~~~ Client pagination (``?page=2``) is enabled by default. This is currently achieved by injecting a ``$facet`` stage contianing two sub-pipelines, -total_count (``$count``) and paginated_results (``$limit`` first, then ``$skip``) -to the very end of the aggregation pipeline after the ``before_aggregation`` hook. -You can turn pagination off by setting ``pagination`` to ``False`` for the endpoint. Keep in mind that, when pagination -is disabled, all aggregation results are included with every response. -Disabling pagination might be appropriate (and actually advisable) only if the -expected response payload is not huge. +total_count (``$count``) and paginated_results (``$limit`` first, then +``$skip``) to the very end of the aggregation pipeline after the +``before_aggregation`` hook. You can turn pagination off by setting +``pagination`` to ``False`` for the endpoint. Keep in mind that, when +pagination is disabled, all aggregation results are included with every +response. Disabling pagination might be appropriate (and actually advisable) +only if the expected response payload is not huge. Client sorting (``?sort=field1``) is not supported at aggregation endpoints. You can of course add one or more ``$sort`` stages to the pipeline, as we did diff --git a/eve/default_settings.py b/eve/default_settings.py index e1ad92496..5d9e98259 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -267,6 +267,6 @@ MONGO_OPTIONS = {"connect": True, "tz_aware": True} # if true, the document will be normalized according to the schema during patch -# this means the fields will be reset to the default value, if not contained in -# the patch body. -NORMALIZE_DOCUMENT_FOR_PATCH = True +# this means fields will be reset their the default value, if any, unless +# contained in the patch body. +NORMALIZE_ON_PATCH = True diff --git a/eve/flaskapp.py b/eve/flaskapp.py index d9998ce76..67c0effb6 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -681,9 +681,7 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault( "normalize_dotted_fields", self.config["NORMALIZE_DOTTED_FIELDS"] ) - settings.setdefault( - "normalize_document_for_patch", self.config["NORMALIZE_DOCUMENT_FOR_PATCH"] - ) + settings.setdefault("normalize_on_patch", self.config["NORMALIZE_ON_PATCH"]) # empty schemas are allowed for read-only access to resources schema = settings.setdefault("schema", {}) self.set_schema_defaults(schema, settings["id_field"]) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 3c416e71d..3ad9d26b8 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -152,7 +152,7 @@ def patch_internal( resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] - normalize_document = resource_def.get("normalize_document_for_patch") + normalize_document = resource_def.get("normalize_on_patch") validator = app.validator( schema, resource=resource, allow_unknown=resource_def["allow_unknown"] ) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 0ba982623..b0a08d4cc 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -246,7 +246,7 @@ test_patch = { "datasource": {"source": "test_patch"}, - "normalize_document_for_patch": False, + "normalize_on_patch": False, "schema": { "name": {"type": "string", "required": True}, "contact": { From cff8ea6030ac651abfe9e7a0db6d6d2dcf2e3960 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 5 May 2019 09:06:35 +0200 Subject: [PATCH 505/821] Changelog for #1261 --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 897794659..e45248cfd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +New +~~~~~ +- ``NORMALIZE_ON_PATCH`` switches normalization on patch requests (`#1234`_) + +.. _`#1234`: https://github.com/pyeve/eve/issues/1234 + Fixed ~~~~~ - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) From a8c84a67685fe59df6a6447aaf4b9cb60f824a32 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 May 2019 11:18:11 +0200 Subject: [PATCH 506/821] Add Blockt to generous backers list --- docs/_static/backers/blokt.png | Bin 0 -> 2249 bytes docs/funding.rst | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/_static/backers/blokt.png diff --git a/docs/_static/backers/blokt.png b/docs/_static/backers/blokt.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1be4dcf85831328a084bebe3247e668b547ccb GIT binary patch literal 2249 zcmV;)2sZbLP)-$00004XF*Lt006O% z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)x010qNS#tmY3labT3lag+-G2N400=EfL_t(|+U=TctdzwS z$A7%|R$41IskXL+z!uOCn9arP7dt}?G-_Y8rq`tHG{vZw8e62d0vADSl^RqKR0!qT zDusc*+z&OWrACP9Oe|(omNh0#tliwkCT$_L#gtmBAoRnWWuLqEdD*vnp|bzv=AL=x z%z0*K|2yZ*IRjmE(M1O0p1 z)kDEn<9s7=Sr_eLDzG5oZ)(z@QN)s6ry3l;1>6EWAngl)&1ti+TKxq0tu#Mxoc~P3 z_9WoZi2BkrTSCIB%lnPhY8kLw+OvRfj{7oq(GcQX;d@Fe184z6R6(TPKE*JU7R;v=}*cg$kf&N3lZG@BRTI0O6S{*sv3)w0p=$?_G zHay;kfp#&1SjPE0VTS^E)oQgV(tSHn7I_;mC&{SQ>Y#CcJFp12+&I6;YPB8suBf~T zd?#&U#`&d@cl^+5bwF(Y)i{3-fv^`6cEDR`4SRF>LY7WR{sxociDn=%%L+1FNzAya zI3#mvIT)7{?cNlXL3z*K3{&;%x3D^|dRO>(ME-WP!)o<1uw9z7jq`VAytE(qm&hB$ z4L&IC6;`W%Bn>pqf0&RzgW!%=*Qsow7SJz}AH-Op2k4VU%-UH}k;@mR627cnnjlpj zdSxy>tqhiNKTlB^tdxt3+g-F%#4^r*8Ms=S&swdXi_u>Rye;zDq(I&{KUv(kG_ znkCjC@rle{+9y#n7zUEQ)O8)0OEs=*hEyeBA?bj38-OnYGl9<;=jU3j zUP&-^H^H?5yb8QdC{mw{=u9Bg^uBJjT5O#Eh1kE|I6q$|{0zYrN5`EYWcKGvdkK)3 zkY!|M|Na&hd?+pmmVk6ziIo(L=kkS4oOsjRX6#N?DHo3b`!WtUiKU{+aLUCk0e(dw z$reHxWdx)sy_P_nzX3f~tHhfL#`&Gb`R`k;ZZghS3AEd4_3=3Sk#QRdG1QgD`3J04 z_r~b-0@q7(kJW0Z17E<|hT%@SzbH-x?^dfl1Y$vmS#FNe-#{%cT9HAj%K!uP8|N<~ z1pWXYGtS>fI#vOPL|$&3pH^pk3bjP*G3rjRT{MgtxqM;USQxHPH$=-Zk3>r*#(4$Y zB+Y|XtDPCPt0%Sdf%#Ueb->@{*-GR5CzA#WVXJQLTtd<|io1bNh`iZq_1ai0&ie>M zgkpXi`o@A>BNC)mtJjS48wknEX@t0?>V^g*s<)*Xts`Lx*dua3;hxrcz!SiYNj9uj zPaEg&0ImXht3_wy{LILl4lz2%)eP1p#4zo~SKD&=LiA8DR4Eq+Q}las`NE2XzP55_ zf)asTN%^b3T)sdHZgJH5Rm#PM5MR<+LpW(&N^nDu0M`JsrGJOjs+{yD+2x*yJo=W^ z>RkY1)BXRvk6`|#eS|!bMfuB+<#k@gw;de1GSL9t*tC#Bfmqz5& z#HNg`e$XD{{0>6;0-$WQ+R^L+w}~j3S~;zCvZmQ|c}Yl6pr2RHA^+2oDe&hUZ;48pfeeGt!o5c|#;XtKaEamcreKNO3F+|vzI*}-CW@kBtO1U^F$93R*TZ(fRZfiyqFsXH`B!JOvw$+BR!bT!Vsa@+wyG(iKWtrIb!Fkl#2s|ovz_| zXWe<u1h18;6cKHU>f1xdz9oSWArBwT*%h<3#p)%;Xd8-=%R}*y6B>dF1qOA%*X!# Xa-GM;{B+vp00000NkvXXu0mjfb>=$* literal 0 HcmV?d00001 diff --git a/docs/funding.rst b/docs/funding.rst index 7352bcc0b..cf3303a1a 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -47,3 +47,11 @@ Just `get in touch`_ with me. .. _`get in touch`: mailto:nicola@nicolaiarocci.com .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course + +Generous Backers +---------------- +Generous backers who actively support Eve and Cerberus development: + +.. image:: _static/backers/blokt.png + :target: http://blokt.com/guides/best-vpn + :alt: Blokt Crypto & Privacy From 7deb977396f69548c112247adbdc6f98117e9098 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 May 2019 11:20:07 +0200 Subject: [PATCH 507/821] Remove --no-use-pep517 option from Makefile As new Black version does not require that anymore. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 37f4c0893..3cdff7f8b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload wheel install-dev: - pip install -q -e .[dev] --no-use-pep517 + pip install -q -e .[dev] test: clean-pyc install-dev pytest From cf9c85342480ba8ca9296233eec61e33013633fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 16 May 2019 18:15:49 +0200 Subject: [PATCH 508/821] Fix crash with Werkzeug >= 0.15.3 Closes #1267 --- CHANGES.rst | 8 +++++--- eve/auth.py | 2 +- eve/endpoints.py | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e45248cfd..510b60fa4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,18 +10,20 @@ New ~~~~~ - ``NORMALIZE_ON_PATCH`` switches normalization on patch requests (`#1234`_) -.. _`#1234`: https://github.com/pyeve/eve/issues/1234 Fixed ~~~~~ +- Creah with Werzeug >= 0.15.3 (`#1267`_) - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) -.. _`#1263`: https://github.com/pyeve/eve/pull/1263 - Improved ~~~~~~~~ - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) +- Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) +.. _`#1234`: https://github.com/pyeve/eve/issues/1234 +.. _`#1267`: https://github.com/pyeve/eve/issues/1267 +.. _`#1263`: https://github.com/pyeve/eve/pull/1263 .. _`#1264`: https://github.com/pyeve/eve/issues/1264 Version 0.9 diff --git a/eve/auth.py b/eve/auth.py index f89244e04..29a29d2f4 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -149,7 +149,7 @@ def authenticate(self): abort( 401, "Please provide proper credentials", - ("WWW-Authenticate", 'Basic realm="%s"' % __package__), + www_authenticate=("WWW-Authenticate", 'Basic realm="%s"' % __package__), ) def authorized(self, allowed_roles, resource, method): diff --git a/eve/endpoints.py b/eve/endpoints.py index 78523df97..91a20c8f4 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -170,7 +170,7 @@ def error_endpoint(error): pass try: - if error.www_authenticate != (None,): + if error.www_authenticate is not None: headers.append(error.www_authenticate) except AttributeError: pass diff --git a/setup.py b/setup.py index bff7a75fd..165c82b4c 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "flask>=1.0", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", - "werkzeug>=0.15.1", + "werkzeug==0.15.3", ] EXTRAS_REQUIRE = { From 4176bf3d86c0b7c1673d861557906d8221397ee6 Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Wed, 15 May 2019 16:08:17 +0200 Subject: [PATCH 509/821] Fix document_etag function mutating the original document --- eve/tests/utils.py | 4 ++++ eve/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/eve/tests/utils.py b/eve/tests/utils.py index 6cf2e6793..82eadebee 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -169,6 +169,7 @@ def test_document_etag(self): def test_document_etag_ignore_fields(self): test = {"key1": "value1", "key2": "value2"} + test_copy = copy.deepcopy(test) ignore_fields = ["key2"] test_without_ignore = {"key1": "value1"} challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") @@ -176,6 +177,7 @@ def test_document_etag_ignore_fields(self): self.assertEqual( hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) ) + self.assertEqual(test, test_copy) # not required fields can not be present test = {"key1": "value1", "key2": "value2"} @@ -189,6 +191,7 @@ def test_document_etag_ignore_fields(self): # ignore fiels nested using doting notation test = {"key1": "value1", "dict": {"key2": "value2", "key3": "value3"}} + test_copy = copy.deepcopy(test) ignore_fields = ["dict.key2"] test_without_ignore = {"key1": "value1", "dict": {"key3": "value3"}} challenge = dumps(test_without_ignore, sort_keys=True).encode("utf-8") @@ -196,6 +199,7 @@ def test_document_etag_ignore_fields(self): self.assertEqual( hashlib.sha1(challenge).hexdigest(), document_etag(test, ignore_fields) ) + self.assertEqual(test, test_copy) # ignore fiels nested using doting notation when a root part of the field is not present test = {"key1": "value1", "dict": {"key2": "value2"}} diff --git a/eve/utils.py b/eve/utils.py index db430c8ab..cd8acde31 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -16,7 +16,7 @@ import eve import hashlib import werkzeug.exceptions -from copy import copy +from copy import deepcopy from flask import request from flask import current_app as app from datetime import datetime, timedelta @@ -352,7 +352,7 @@ def filter_ignore_fields(d, fields): # not required fields can be not present pass - value_ = copy(value) + value_ = deepcopy(value) filter_ignore_fields(value_, ignore_fields) else: value_ = value From 5a7eb09142b28e1bfc9f834d160ab279f73a247c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 16 May 2019 18:25:53 +0200 Subject: [PATCH 510/821] Changelog for #1266 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 510b60fa4..58fda98e0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,8 @@ New Fixed ~~~~~ -- Creah with Werzeug >= 0.15.3 (`#1267`_) +- If ``ignore_fields`` contains a nested field, document is mutated (`#1266`_) +- Crash with Werzeug >= 0.15.3 (`#1267`_) - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) Improved @@ -21,6 +22,7 @@ Improved - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) - Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) +.. _`#1266`: https://github.com/pyeve/eve/pull/1266 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1267`: https://github.com/pyeve/eve/issues/1267 .. _`#1263`: https://github.com/pyeve/eve/pull/1263 From fd83c88f5e555084ce91454daded434ba0a6c686 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 20 May 2019 11:57:32 +0200 Subject: [PATCH 511/821] Fix: document count broken with concurrent requests Closes #1271 --- CHANGES.rst | 15 +++++++++++- eve/io/base.py | 4 ++- eve/io/mongo/mongo.py | 49 +++++++++++++++++-------------------- eve/methods/common.py | 7 +++--- eve/methods/delete.py | 3 ++- eve/methods/get.py | 14 +++++------ eve/tests/methods/delete.py | 16 ++++++------ eve/tests/methods/get.py | 6 ++--- 8 files changed, 63 insertions(+), 51 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 58fda98e0..90ca381ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,9 +10,9 @@ New ~~~~~ - ``NORMALIZE_ON_PATCH`` switches normalization on patch requests (`#1234`_) - Fixed ~~~~~ +- Document count broken with concurrent requests (`#1271`_) - If ``ignore_fields`` contains a nested field, document is mutated (`#1266`_) - Crash with Werzeug >= 0.15.3 (`#1267`_) - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) @@ -22,6 +22,19 @@ Improved - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) - Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) +Breaking Changes +~~~~~~~~~~~~~~~~ + +No known breaking changes for the standard framework user. However, if you are +consuming the developer API: + +- Be aware that ``io.base.DataLayer.find()`` signature has changed and an + optional ``perform_count`` argument has been added. The method return value + is now a tuple ``(cursor, count)``; ``cursor`` is the query result as + before while ``count`` is the document count, which is expected to have a + consistent value when ``perform_count = True``. + +.. _`#1271`: https://github.com/pyeve/eve/issues/1271 .. _`#1266`: https://github.com/pyeve/eve/pull/1266 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1267`: https://github.com/pyeve/eve/issues/1267 diff --git a/eve/io/base.py b/eve/io/base.py index 26ee6077c..82bf793b4 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -117,7 +117,7 @@ def init_app(self, app): """ raise NotImplementedError - def find(self, resource, req, sub_resource_lookup): + def find(self, resource, req, sub_resource_lookup, perform_count=True): """ Retrieves a set of documents (rows), matching the current request. Consumed when a request hits a collection/document endpoint (`/people/`). @@ -134,6 +134,8 @@ def find(self, resource, req, sub_resource_lookup): to support with your driver. For example ``eve.io.Mongo`` supports both Python and Mongo-like query syntaxes. :param sub_resource_lookup: sub-resource lookup from the endpoint url. + :param perform_count: wether a document count should be performed and + returned to the client. .. versionchanged:: 0.3 Support for sub-resources. diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 1f79eb4e6..516253528 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -141,7 +141,7 @@ def init_app(self, app): self.driver = PyMongos(self) self.mongo_prefix = None - def find(self, resource, req, sub_resource_lookup): + def find(self, resource, req, sub_resource_lookup, perform_count=True): """ Retrieves a set of documents matching a given request. Queries can be expressed in two different formats: the mongo query syntax, and the python syntax. The first kind of query would look like: :: @@ -258,9 +258,9 @@ def find(self, resource, req, sub_resource_lookup): if projection: args["projection"] = projection - self.__last_target = self.pymongo(resource).db[datasource], spec + target = self.pymongo(resource).db[datasource] try: - self.__last_cursor = self.pymongo(resource).db[datasource].find(**args) + result = target.find(**args) except TypeError as e: # pymongo raises ValueError when invalid query paramenters are # included. We do our best to catch them beforehand but, especially @@ -268,30 +268,27 @@ def find(self, resource, req, sub_resource_lookup): self.app.logger.exception(e) abort(400, description=debug_error_message(str(e))) - return self.__last_cursor - - @property - def last_documents_count(self): - if not self.__last_target: - return None + if perform_count: + try: + count = target.count_documents(spec) + except: + # fallback to deprecated method. this might happen when the query + # includes operators not supported by count_documents(). one + # documented use-case is when we're running on mongo 3.4 and below, + # which does not support $expr ($expr must replace $where # in + # count_documents()). + + # 1. Mongo 3.6+; $expr: pass + # 2. Mongo 3.6+; $where: pass (via fallback) + # 3. Mongo 3.4; $where: pass (via fallback) + # 4. Mongo 3.4; $expr: fail (operator not supported by db) + + # See: http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.count + count = target.count() + else: + count = None - try: - target, spec = self.__last_target - return target.count_documents(spec) - except: - # fallback to deprecated method. this might happen when the query - # includes operators not supported by count_documents(). one - # documented use-case is when we're running on mongo 3.4 and below, - # which does not support $expr ($expr must replace $where # in - # count_documents()). - - # 1. Mongo 3.6+; $expr: pass - # 2. Mongo 3.6+; $where: pass (via fallback) - # 3. Mongo 3.4; $where: pass (via fallback) - # 4. Mongo 3.4; $expr: fail (operator not supported by db) - - # See: http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.count - return self.__last_cursor.count() + return result, count def find_one( self, diff --git a/eve/methods/common.py b/eve/methods/common.py index 72f3802e2..8eb700ab7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -849,7 +849,7 @@ def embedded_document(references, data_relation, field_name): :param data_relation: the relation schema definition. :param field_name: field name used in abort message only - .. versionadded:: 0.5 +) .. versionadded:: 0.5 """ embedded_docs = [] @@ -892,9 +892,10 @@ def embedded_document(references, data_relation, field_name): data_relation, references ) for subresource in subresources_query: - list_embedded_doc = list( - app.data.find(subresource, None, subresources_query[subresource]) + result, _ = app.data.find( + subresource, None, subresources_query[subresource] ) + list_embedded_doc = list(result) if not list_embedded_doc: embedded_docs.extend( diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 3ea72bc1b..8f5c40559 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -217,7 +217,8 @@ def delete(resource, **lookup): # get_document should always fetch soft deleted documents from the db # callers must handle soft deleted documents default_request.show_deleted = True - originals = list(app.data.find(resource, default_request, lookup)) + result, _ = app.data.find(resource, default_request, lookup) + originals = list(result) if not originals: abort(404) # I add new callback as I want the framework to be retro-compatible diff --git a/eve/methods/get.py b/eve/methods/get.py index 83bdc1a35..96b79b1a1 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -262,7 +262,9 @@ def _perform_find(resource, lookup): # If-Modified-Since disabled on collections (#334) req.if_modified_since = None - cursor = app.data.find(resource, req, lookup) + cursor, count = app.data.find( + resource, req, lookup, perform_count=not config.OPTIMIZE_PAGINATION_FOR_SPEED + ) # If soft delete is enabled, data.find will not include items marked # deleted unless req.show_deleted is True for document in cursor: @@ -279,10 +281,7 @@ def _perform_find(resource, lookup): response[config.ITEMS] = documents - if config.OPTIMIZE_PAGINATION_FOR_SPEED: - count = None - else: - count = app.data.last_documents_count + if count: headers.append((config.HEADER_TOTAL_COUNT, count)) if config.DOMAIN[resource]["hateoas"]: @@ -454,11 +453,11 @@ def getitem_internal(resource, **lookup): # default sort for 'all', required sort for 'diffs' req.sort = '[("%s", 1)]' % config.VERSION req.if_modified_since = None # we always want the full history here - cursor = app.data.find(resource + config.VERSIONS, req, lookup) + cursor, count = app.data.find(resource + config.VERSIONS, req, lookup) # build all versions documents = [] - if app.data.last_documents_count == 0: + if count == 0: # this is the scenario when the document existed before # document versioning got turned on documents.append(latest_doc) @@ -510,7 +509,6 @@ def getitem_internal(resource, **lookup): if config.DOMAIN[resource]["hateoas"]: # use the id of the latest document for multi-document requests if cursor: - count = app.data.last_documents_count response[config.LINKS] = _pagination_links( resource, req, count, latest_doc[resource_def["id_field"]] ) diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index c301f6cc8..424858b43 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -559,24 +559,24 @@ def test_softdelete_datalayer(self): # show_deleted == True is passed or if the deleted field is part of # the lookup req.show_deleted = False - self.app.data.find(self.known_resource, req, None) - undeleted_count = self.app.data.last_documents_count + _, undeleted_count = self.app.data.find(self.known_resource, req, None) req.show_deleted = True - self.app.data.find(self.known_resource, req, None) - self.assertEqual(undeleted_count, self.app.data.last_documents_count - 1) + _, challenge = self.app.data.find(self.known_resource, req, None) + self.assertEqual(undeleted_count, challenge - 1) req.show_deleted = False - self.app.data.find(self.known_resource, req, {self.deleted_field: True}) - deleted_count = self.app.data.last_documents_count + _, deleted_count = self.app.data.find( + self.known_resource, req, {self.deleted_field: True} + ) self.assertEqual(deleted_count, 1) # find_list_of_ids will return deleted documents if given their id - self.app.data.find_list_of_ids( + ids = self.app.data.find_list_of_ids( self.known_resource, [ObjectId(self.item_id)] ) - self.assertEqual(self.app.data.last_documents_count, 1) + self.assertEqual(str(ids[0]["_id"]), self.item_id) def test_softdelete_db_fields(self): """Documents created when soft delete is enabled should include and diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 4e4b2a1f0..0fa7dea16 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1051,13 +1051,13 @@ def test_cursor_extra_find(self): _find = self.app.data.find hits = {"total_hits": 0} - def find(resource, req, sub_resource_lookup): + def find(resource, req, sub_resource_lookup, perform_count=True): def extra(response): response["_hits"] = hits - cursor = _find(resource, req, sub_resource_lookup) + cursor, _ = _find(resource, req, sub_resource_lookup) cursor.extra = extra - return cursor + return cursor, _ self.app.data.find = find r, status = self.get(self.known_resource) From 19d3988f4939cb4646ba51bd50823f1fd2534ed2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 16:00:44 +0200 Subject: [PATCH 512/821] Changelog for #1268 fix --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 90ca381ce..34fe80730 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,7 @@ New Fixed ~~~~~ - Document count broken with concurrent requests (`#1271`_) +- Document count broken when embedded resources are requested (`#1268`_) - If ``ignore_fields`` contains a nested field, document is mutated (`#1266`_) - Crash with Werzeug >= 0.15.3 (`#1267`_) - Fix crash when trying to ignore a nested field that doesn't exist (`#1263`_) @@ -35,6 +36,7 @@ consuming the developer API: consistent value when ``perform_count = True``. .. _`#1271`: https://github.com/pyeve/eve/issues/1271 +.. _`#1268`: https://github.com/pyeve/eve/issues/1268 .. _`#1266`: https://github.com/pyeve/eve/pull/1266 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1267`: https://github.com/pyeve/eve/issues/1267 From a4caccff1372822bc90b2571499bfee1c131409a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 16:06:01 +0200 Subject: [PATCH 513/821] Quickstart: a better MONGO_AUTH_SOURCE explanation Closes #1168 --- CHANGES.rst | 2 ++ docs/quickstart.rst | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 34fe80730..86ea5a719 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,7 @@ Improved ~~~~~~~~ - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) - Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) +- Quickstart: a better ``MONGO_AUTH_SOURCE`` explanation (`#1168`_) Breaking Changes ~~~~~~~~~~~~~~~~ @@ -37,6 +38,7 @@ consuming the developer API: .. _`#1271`: https://github.com/pyeve/eve/issues/1271 .. _`#1268`: https://github.com/pyeve/eve/issues/1268 +.. _`#1168`: https://github.com/pyeve/eve/issues/1168 .. _`#1266`: https://github.com/pyeve/eve/pull/1266 .. _`#1234`: https://github.com/pyeve/eve/issues/1234 .. _`#1267`: https://github.com/pyeve/eve/issues/1267 diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f4b0bf5af..950f81543 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -128,10 +128,12 @@ Let's connect to a database by adding the following lines to settings.py: MONGO_HOST = 'localhost' MONGO_PORT = 27017 - # Skip these if your db has no auth. But it really should. + # Skip this block if your db has no auth. But it really should. MONGO_USERNAME = '' MONGO_PASSWORD = '' - MONGO_AUTH_SOURCE = 'admin' # needed if --auth mode is enabled + # Name of the database on which the user can be authenticated, + # needed if --auth mode is enabled. + MONGO_AUTH_SOURCE = '' MONGO_DBNAME = 'apitest' From 409ff6b83c3d811def278338e3ffb253f410a09e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 16:33:52 +0200 Subject: [PATCH 514/821] changelog typo --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 86ea5a719..5366c864b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,7 +21,7 @@ Fixed Improved ~~~~~~~~ - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) -- Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) +- Bump (and pin) Wekzeug to 0.15.3 (`#1267`_) - Quickstart: a better ``MONGO_AUTH_SOURCE`` explanation (`#1168`_) Breaking Changes From 37295c3b441d1db8218f6cbd37d460b645b29140 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 17:07:51 +0200 Subject: [PATCH 515/821] Bump and pin Werkzeug to v0.15.4 --- CHANGES.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5366c864b..86ea5a719 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,7 +21,7 @@ Fixed Improved ~~~~~~~~ - Remove unsupported ``transparent_schema_rules`` option from docs (`#1264`_) -- Bump (and pin) Wekzeug to 0.15.3 (`#1267`_) +- Bump (and pin) Wekzeug to 0.15.4 (`#1267`_) - Quickstart: a better ``MONGO_AUTH_SOURCE`` explanation (`#1168`_) Breaking Changes diff --git a/setup.py b/setup.py index 165c82b4c..dbc10479b 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "flask>=1.0", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", - "werkzeug==0.15.3", + "werkzeug==0.15.4", ] EXTRAS_REQUIRE = { From bc1d8561cf08fa6a9f89d2ca22195192f530a814 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 16:25:39 +0200 Subject: [PATCH 516/821] v0.9.1 release date --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 86ea5a719..a24caf3e7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 0.9.1 +------------- + +Released on May 22, 2019. + New ~~~~~ - ``NORMALIZE_ON_PATCH`` switches normalization on patch requests (`#1234`_) From 292e94372ef2ab0a45f923200ea12a5b39f4de28 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 May 2019 16:26:05 +0200 Subject: [PATCH 517/821] Bump version to 0.9.1 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 6c1888121..ff87beefd 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9" +__version__ = "0.9.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 6f2d58a5d633697b75ab80989fa3fd3c18b7d804 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Wed, 29 May 2019 12:09:44 +0200 Subject: [PATCH 518/821] Fix HEADER_TOTAL_COUNT on emtpy resources the count returned by `app.find` might be None if the count is disabled. Later in get_internal, the HEADER_TOTAL_COUNT is prepared if the count is enabled. This commit fixes a bug that filtered out the HEADER_TOTAL_COUNT when the resource is empty and the count is 0, replacing a truthy condition by an explicit `is not None` condition. --- eve/methods/get.py | 2 +- eve/tests/methods/get.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 96b79b1a1..708b360b8 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -281,7 +281,7 @@ def _perform_find(resource, lookup): response[config.ITEMS] = documents - if count: + if count is not None: headers.append((config.HEADER_TOTAL_COUNT, count)) if config.DOMAIN[resource]["hateoas"]: diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 0fa7dea16..e89fc32fc 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -169,6 +169,17 @@ def test_get_total_count_header(self): total_count = r.headers[self.app.config["HEADER_TOTAL_COUNT"]] self.assertEqual(int(total_count), self.known_resource_count) + def test_get_total_count_header_on_empty_resource(self): + url = self.domain[self.empty_resource]["url"] + r = self.test_client.head(url) + response, status = self.parse_response(r) + self.assert200(status) + self.assertEqual(response, None) + + self.assertIn(self.app.config["HEADER_TOTAL_COUNT"], r.headers) + total_count = r.headers[self.app.config["HEADER_TOTAL_COUNT"]] + self.assertEqual(int(total_count), 0) + def test_get_where_mongo_syntax(self): where = '{"ref": "%s"}' % self.item_name response, status = self.get(self.known_resource, "?where=%s" % where) From 600de0ec19f45cc1daadaf9760930f14c6ff3a91 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 3 Jun 2019 17:04:51 +0200 Subject: [PATCH 519/821] Changelog for #1276 --- CHANGES.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a24caf3e7..3db611737 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +Fixed +~~~~~ +- The condition that avoids returning ``X-Total-Count`` when counting is + disabled also filters out the case where the resource is empty and count is + 0 (`#1279`_) + +.. _`#1279`: https://github.com/pyeve/eve/issues/1279 Version 0.9.1 ------------- From dd726c26c68324c5ac1a15d2de431b0a660333ca Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Tue, 4 Jun 2019 23:22:13 +0200 Subject: [PATCH 520/821] Fixes behaviour of PUT requests not setting default values of fields. * Refactor of "contacts" test data so it honours the fields that should have defaults when created * Added a new fields in "contacts" schema to be able to properly refactor tests related to default values * Added tests to cover several scenarios where default values are involved --- eve/tests/__init__.py | 5 +++ eve/tests/methods/patch.py | 89 +++++++++++++++++++++++++++++--------- eve/tests/methods/post.py | 24 +++++++++- eve/tests/methods/put.py | 54 ++++++++++++++++++++++- eve/tests/test_settings.py | 14 ++++++ eve/tests/versioning.py | 1 + eve/validation.py | 27 +++++++++--- 7 files changed, 185 insertions(+), 29 deletions(-) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 20936793f..7d47059e0 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -495,6 +495,7 @@ def random_contacts(self, num, standard_date_fields=True): "ref": self.random_string(schema["ref"]["maxlength"]), "prog": i, "role": random.choice(schema["role"]["allowed"]), + "title": schema["title"]["default"], "rows": self.random_rows(random.randint(0, 5)), "alist": self.random_list(random.randint(0, 5)), "location": { @@ -504,6 +505,10 @@ def random_contacts(self, num, standard_date_fields=True): "born": datetime.today() + timedelta(days=random.randint(-10, 10)), "tid": ObjectId(), "read_only_field": schema["read_only_field"]["default"], + "dependency_field1": schema["dependency_field1"]["default"], + # The schema for contacts has a field named 'unsetted_default_value_field' + # That is not initialized here on purpose. See put test on + # tests.put.put_default_value_when_field_missing } if standard_date_fields: contact[eve.LAST_UPDATED] = dt diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index b2b898703..317cc5b55 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -228,7 +228,12 @@ def test_patch_missing_default(self): test_value = "1234567890123456789012345" changes = {field: test_value} r = self.perform_patch(changes) - self.assertEqual(self.compare_patch_with_get("title", r), "Mr.") + self.assertEqual( + self.compare_patch_with_get("unsetted_default_value_field", r), + self.domain["contacts"]["schema"]["unsetted_default_value_field"][ + "default" + ], + ) def test_patch_missing_default_with_post_override(self): """ PATCH an object which is missing a field with a default value. @@ -239,8 +244,32 @@ def test_patch_missing_default_with_post_override(self): test_value = "1234567890123456789012345" r = self.perform_patch_with_post_override(field, test_value) self.assert200(r.status_code) - title = self.compare_patch_with_get("title", json.loads(r.get_data())) - self.assertEqual(title, "Mr.") + unsetted_default_value_field = self.compare_patch_with_get( + "unsetted_default_value_field", json.loads(r.get_data()) + ) + self.assertEqual( + unsetted_default_value_field, + self.domain["contacts"]["schema"]["unsetted_default_value_field"][ + "default" + ], + ) + + def test_patch_missing_nested_default(self): + """ PATCH an object which is missing a field with a default value. + + This should result in setting the field to its default value, even if + the field is not provided in the PATCH's payload. """ + field = "dict_with_nested_default" + test_value = {} + changes = {field: test_value} + r = self.perform_patch(changes) + + item_id = r[self.domain[self.known_resource]["id_field"]] + raw_r = self.test_client.get("%s/%s" % (self.known_resource_url, item_id)) + item, status = self.parse_response(raw_r) + self.assertEqual( + item["dict_with_nested_default"], {"nested_field_with_default": "nested"} + ) def test_patch_multiple_fields(self): fields = ["ref", "prog", "role"] @@ -646,41 +675,61 @@ def test_patch_nested_document_nullable_missing(self): self.assertEqual(r["other"], {"name": "other_name"}) def test_patch_dependent_field_on_origin_document(self): - """ Test that when patching a field which is dependent on another and - this other field is not provided with the patch but is still present - on the target document, the patch will be accepted. See #363. + """ Test that when patching a field which is dependent on another field's + existance, and this other field is not provided in the patch, but does + exist on the persisted document, the patch will be accepted. + + The value on the document can be there either because is was set + explicitly or because it was set as a default value by Eve. + + See #363. """ - # this will fail as dependent field is missing even in the - # document we are trying to update. - del self.domain["contacts"]["schema"]["dependency_field1"]["default"] + + # this will succeed as even if the value is not present in the PATCH + # payload, it is in the persisted document because the dependency_field1 + # had a default value defined changes = {"dependency_field2": "value"} r, status = self.patch( self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] ) + self.assert200(status) + + # this will fail, as dependent field is missing in the PATCH payload + # and is not present in the persisted document (it doesn't even have a + # default value) + etag = r["_etag"] + changes = {"dependency_field5": "value"} + r, status = self.patch( + self.item_id_url, data=changes, headers=[("If-Match", etag)] + ) self.assert422(status) - # update the stored document by adding dependency field. - changes = {"dependency_field1": "value"} + # update the stored document by adding the dependency field with some + # unknown value + changes = {"dependency_field4": "unknown_value"} r, status = self.patch( - self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] + self.item_id_url, data=changes, headers=[("If-Match", etag)] ) self.assert200(status) - # now the field2 update will be accepted as the dependency field is - # present in the stored document already. + # This will succeed as now the field is present in the persisted document + # even if it's not provided in the patch payload etag = r["_etag"] - changes = {"dependency_field2": "value"} + changes = {"dependency_field5": "value"} r, status = self.patch( self.item_id_url, data=changes, headers=[("If-Match", etag)] ) self.assert200(status) def test_patch_dependent_field_value_on_origin_document(self): - """ Test that when patching a field which is dependent on another and - this other field is not provided with the patch but is still present - on the target document, the patch will be accepted. See #363. + """ Test that when patching a field which is dependent on another field's + value, and this other field is not provided in the patch, but is present + on the persisted document, the patch will be accepted. + + See #363. """ - # this will fail as dependent field is missing even in the + + # this will fail as the dependent field has value that doesn't # document we are trying to update. changes = {"dependency_field3": "value"} r, status = self.patch( @@ -696,7 +745,7 @@ def test_patch_dependent_field_value_on_origin_document(self): ) self.assert200(status) - # now the field2 update will be accepted as the dependency field is + # now the field3 update will be accepted as the dependency field is # present in the stored document already. etag = r["_etag"] changes = {"dependency_field3": "value"} diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index a87c11f63..412a967c6 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -145,8 +145,10 @@ def test_post_null_objectid(self): self.assertPostItem(data, test_field, test_value) def test_post_default_value(self): - test_field = "title" - test_value = "Mr." + test_field = "unsetted_default_value_field" + test_value = self.domain["contacts"]["schema"]["unsetted_default_value_field"][ + "default" + ] data = {"ref": "9234567890123456789054321"} self.assertPostItem(data, test_field, test_value) @@ -768,6 +770,24 @@ def test_post_readonly_field_with_default(self): r, status = self.post(self.known_resource_url, data=data) self.assertValidationErrorStatus(status) + def test_post_with_nested_default(self): + """ Test that in post of a field that has nested fields with default values + those default values are set + """ + del self.domain["contacts"]["schema"]["ref"]["required"] + test_field = "dict_with_nested_default" + test_value = {} + data = {test_field: test_value} + r, status = self.post(self.known_resource_url, data=data) + self.assert201(status) + + item_id = r[self.domain[self.known_resource]["id_field"]] + raw_r = self.test_client.get("%s/%s" % (self.known_resource_url, item_id)) + item, status = self.parse_response(raw_r) + self.assertEqual( + item["dict_with_nested_default"], {"nested_field_with_default": "nested"} + ) + def test_post_readonly_in_dict(self): # Test that a post with a readonly field inside a dict is properly # validated (even if it has a defult value) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index b7c5f6497..ddb0d6a58 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -196,7 +196,30 @@ def test_put_with_post_override(self): self.assert200(r.status_code) self.assertPutResponse(json.loads(r.get_data()), self.item_id) - def test_put_default_value(self): + def test_put_sets_default_value_when_field_not_provided_neither_persisted(self): + """ + Test that when replacing a document, any field that has default values + defined in the schema is set according to the schema default when + the current persisted document doesn't have the field value set. + """ + test_field = "unsetted_default_value_field" + test_value = self.domain["contacts"]["schema"]["unsetted_default_value_field"][ + "default" + ] + data = {"ref": "9234567890123456789054321"} + r = self.perform_put(data) + db_value = self.compare_put_with_get(test_field, r) + self.assertEqual(test_value, db_value) + + def test_put_sets_default_value_when_field_not_provided_but_persisted(self): + """ + Test that when replacing a document, any field that has default values + defined in the schema is set according to the schema default when + the current persisted document already had the field value set. + + This effectively makes impossible to delete fields with default values + in the schema using a PUT request. + """ test_field = "title" test_value = "Mr." data = {"ref": "9234567890123456789054321"} @@ -204,6 +227,35 @@ def test_put_default_value(self): db_value = self.compare_put_with_get(test_field, r) self.assertEqual(test_value, db_value) + def test_put_removes_non_provided_non_default_field(self): + """ + Test that when replacing a document, any field that has doesn't have + a default value defined in the schema and has not been provided in + the request will be effectively deleted in the replaced version. + + """ + data = {"ref": "9234567890123456789054321"} + r = self.perform_put(data) + + item_id = r[self.domain[self.known_resource]["id_field"]] + raw_r = self.test_client.get("%s/%s" % (self.known_resource_url, item_id)) + item, status = self.parse_response(raw_r) + + meta_fields = ["_etag", "_updated", "_id", "_links", "_created"] + explicitly_set_fields = ["ref"] + fields_with_defaults = [ + "unsetted_default_value_field", + "ref", + "dependency_field1", + "title", + "read_only_field", + ] + + self.assertEqual( + set(meta_fields + explicitly_set_fields + fields_with_defaults), + set(item.keys()), + ) + def test_put_readonly_value_same(self): data = { "ref": self.item["ref"], diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index b0a08d4cc..fcf75d257 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -85,6 +85,12 @@ "type": "string", "dependencies": {"dependency_field1": "value"}, }, + "dependency_field4": {"type": "string"}, + "dependency_field5": {"type": "string", "dependencies": ["dependency_field4"]}, + "dependency_field6": { + "type": "string", + "dependencies": {"dependency_field4": "value"}, + }, "read_only_field": {"type": "string", "default": "default", "readonly": True}, "dict_with_read_only": { "type": "dict", @@ -96,6 +102,13 @@ } }, }, + "dict_with_nested_default": { + "type": "dict", + "schema": { + "nested_field": {"type": "string"}, + "nested_field_with_default": {"type": "string", "default": "nested"}, + }, + }, "key1": {"type": "string"}, "keyschema_dict": { "type": "dict", @@ -112,6 +125,7 @@ "schema": {"challenge": {"type": "objectid"}}, }, }, + "unsetted_default_value_field": {"type": "string", "default": "value"}, }, } diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 460fa45ce..ed499e19a 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -32,6 +32,7 @@ def tearDown(self): def enableVersioning(self, partial=False): del self.domain["contacts"]["schema"]["title"]["default"] del self.domain["contacts"]["schema"]["dependency_field1"]["default"] + del self.domain["contacts"]["schema"]["unsetted_default_value_field"]["default"] del self.domain["contacts"]["schema"]["read_only_field"]["default"] del self.domain["contacts"]["schema"]["dict_with_read_only"]["schema"][ "read_only_in_dict" diff --git a/eve/validation.py b/eve/validation.py index 811d393a1..4744b1e5d 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -25,6 +25,7 @@ def __init__(self, *args, **kwargs): if not config.VALIDATION_ERROR_AS_LIST: kwargs["error_handler"] = SingleErrorAsStringErrorHandler + self.is_update_operation = False super(Validator, self).__init__(*args, **kwargs) def validate_update( @@ -38,6 +39,7 @@ def validate_update( :param persisted_document: the persisted document to be updated. :param normalize_document: whether apply normalization during patch. """ + self.is_update_operation = True self.document_id = document_id self.persisted_document = persisted_document return super(Validator, self).validate( @@ -65,13 +67,26 @@ def validate_replace(self, document, document_id, persisted_document=None): def _normalize_default(self, mapping, schema, field): """ {'nullable': True} """ - challenge = self.persisted_document - if challenge: - for sub_field in self.document_path: - challenge = challenge[sub_field] + # fields with no default are of no use here + if "default" not in schema[field]: + return - if not challenge or field not in challenge: - super(Validator, self)._normalize_default(mapping, schema, field) + # if the request already contains the field, we don't set any default + if field in mapping: + return + + # Field already set, we don't want to override with a default on an update + if self.is_update_operation and field in self.persisted_document: + return + + # If we reach here we are processing a field that has a default in the schema + # and the request doesn't explicitly set it. So we are in one of this cases: + # + # - An initial POST + # - A PATCH to an existing document where the field is not set + # - A PUT to a document where the field maybe is set + + super(Validator, self)._normalize_default(mapping, schema, field) def _normalize_default_setter(self, mapping, schema, field): """ {'oneof': [ From 9b0c02c2f1f160efd3af52b88a7e9f07db022f2d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Jun 2019 15:13:37 +0200 Subject: [PATCH 521/821] Changelog fro #1282 --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3db611737..f25e7132a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,10 +8,16 @@ In Development Fixed ~~~~~ +- PUT requests doesn't set default values for fields that have one defined + (`#1280`_) +- PATCH crashes when normalizing default fields (`#1275`_, `#1274`_) - The condition that avoids returning ``X-Total-Count`` when counting is disabled also filters out the case where the resource is empty and count is 0 (`#1279`_) +.. _`#1280`: https://github.com/pyeve/eve/issues/1280 +.. _`#1275`: https://github.com/pyeve/eve/issues/1275 +.. _`#1274`: https://github.com/pyeve/eve/issues/1274 .. _`#1279`: https://github.com/pyeve/eve/issues/1279 Version 0.9.1 From 4616988d08fbbf542288bd7bfab60efa976d19fe Mon Sep 17 00:00:00 2001 From: Alberto Marin Date: Thu, 6 Jun 2019 12:46:15 -0400 Subject: [PATCH 522/821] Pass Combined Args to Pre Event Hooks --- eve/methods/common.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 8eb700ab7..eb17ced70 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1332,9 +1332,14 @@ def decorated(*args, **kwargs): resource = args[0] if args else None gh_params = () rh_params = () + combined_args = kwargs + + if len(args) > 1: + combined_args.update(args[1].items()) + if method in ("GET", "PATCH", "DELETE", "PUT"): - gh_params = (resource, request, kwargs) - rh_params = (request, kwargs) + gh_params = (resource, request, combined_args) + rh_params = (request, combined_args) elif method in ("POST",): # POST hook does not support the kwargs argument gh_params = (resource, request) @@ -1346,9 +1351,6 @@ def decorated(*args, **kwargs): # resource hook getattr(app, event_name + "_" + resource)(*rh_params) - combined_args = kwargs - if len(args) > 1: - combined_args.update(args[1].items()) r = f(resource, **combined_args) return r From db33e82028711949e3e964e43968ba2171450119 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Jun 2019 15:20:59 +0200 Subject: [PATCH 523/821] Changelog for #1284 --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f25e7132a..c39512c77 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ In Development Fixed ~~~~~ +- Lookup argument does not get passed to ``pre_`` hook with certain + resource urls (`#1283`_) - PUT requests doesn't set default values for fields that have one defined (`#1280`_) - PATCH crashes when normalizing default fields (`#1275`_, `#1274`_) @@ -15,6 +17,7 @@ Fixed disabled also filters out the case where the resource is empty and count is 0 (`#1279`_) +.. _`#1283`: https://github.com/pyeve/eve/issues/1283 .. _`#1280`: https://github.com/pyeve/eve/issues/1280 .. _`#1275`: https://github.com/pyeve/eve/issues/1275 .. _`#1274`: https://github.com/pyeve/eve/issues/1274 From 1587782788ebd21421435c08579c3e2c28df4969 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Jun 2019 15:22:03 +0200 Subject: [PATCH 524/821] Alberto Marin --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index ec3c34192..c2ec21a2f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,8 +9,8 @@ Development Lead Patches and Contributions ````````````````````````` - - Aayush Sarva +- Alberto Marin - Alexander Dietmüller - Alexander Hendorf - Amedeo Bussi From 9ccb4bbbe978eedf0232cafe36044d91ae7c554a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Jun 2019 15:25:52 +0200 Subject: [PATCH 525/821] Add support for Mongo's minDistance query operator Closes #1281 --- CHANGES.rst | 2 ++ eve/io/mongo/mongo.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c39512c77..df9a30200 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ In Development Fixed ~~~~~ +- Geo queries lack support for the ``$minDistance`` mongo operator (`#1281`_) - Lookup argument does not get passed to ``pre_`` hook with certain resource urls (`#1283`_) - PUT requests doesn't set default values for fields that have one defined @@ -18,6 +19,7 @@ Fixed 0 (`#1279`_) .. _`#1283`: https://github.com/pyeve/eve/issues/1283 +.. _`#1281`: https://github.com/pyeve/eve/issues/1281 .. _`#1280`: https://github.com/pyeve/eve/issues/1280 .. _`#1275`: https://github.com/pyeve/eve/issues/1275 .. _`#1274`: https://github.com/pyeve/eve/issues/1274 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 516253528..ee05579c7 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -122,7 +122,7 @@ class Mongo(DataLayer): + ["$options", "$search", "$language", "$caseSensitive"] + ["$diacriticSensitive", "$exists", "$type"] + ["$geoWithin", "$geoIntersects", "$near", "$nearSphere", "$centerSphere"] - + ["$geometry", "$maxDistance", "$box"] + + ["$geometry", "$maxDistance", "$minDistance", "$box"] + ["$all", "$elemMatch", "$size"] + ["$bitsAllClear", "$bitsAllSet", "$bitsAnyClear", "$bitsAnySet"] + ["$center", "$expr"] From 2aa5746c0f1c1e597459581ebaef29849fcbc937 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Jun 2019 15:39:06 +0200 Subject: [PATCH 526/821] Fix: homepage example of Eve does not really work Closes #1277 --- CHANGES.rst | 2 ++ docs/index.rst | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index df9a30200..a117a36c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,10 +17,12 @@ Fixed - The condition that avoids returning ``X-Total-Count`` when counting is disabled also filters out the case where the resource is empty and count is 0 (`#1279`_) +- First example of Eve use doesn't really work (`#1277`_) .. _`#1283`: https://github.com/pyeve/eve/issues/1283 .. _`#1281`: https://github.com/pyeve/eve/issues/1281 .. _`#1280`: https://github.com/pyeve/eve/issues/1280 +.. _`#1277`: https://github.com/pyeve/eve/issues/1277 .. _`#1275`: https://github.com/pyeve/eve/issues/1275 .. _`#1274`: https://github.com/pyeve/eve/issues/1274 .. _`#1279`: https://github.com/pyeve/eve/issues/1279 diff --git a/docs/index.rst b/docs/index.rst index e45c4ef6b..6ce13b2ed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,7 +43,9 @@ Eve is Simple from eve import Eve - app = Eve() + settings = {'DOMAIN': {'people': {}}} + + app = Eve(settings=settings) app.run() The API is now live, ready to be consumed: @@ -54,8 +56,8 @@ The API is now live, ready to be consumed: HTTP/1.1 200 OK All you need to bring your API online is a database, a configuration file -(defaults to ``settings.py``) and a launch script. Overall, you will find that -configuring and fine-tuning your API is a very simple process. +(defaults to ``settings.py``) or dictionary, and a launch script. Overall, you +will find that configuring and fine-tuning your API is a very simple process. Funding Eve ----------- From b9e8d785e65ff7d7e7e3812a4df077960b1daf03 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 14 Jun 2019 14:35:51 +0200 Subject: [PATCH 527/821] Bump version to 0.9.2 --- CHANGES.rst | 9 +++++++++ eve/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a117a36c7..35ed4a846 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,17 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 0.9.2 +------------- + +Released on June 14, 2019. + Fixed ~~~~~ + + - Geo queries lack support for the ``$minDistance`` mongo operator (`#1281`_) - Lookup argument does not get passed to ``pre_`` hook with certain resource urls (`#1283`_) diff --git a/eve/__init__.py b/eve/__init__.py index ff87beefd..f9f9d9b34 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9.1" +__version__ = "0.9.2" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 4b6ccd4e9f7c22d1e75754b2063aa4e0e5a48101 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Mon, 1 Jul 2019 00:23:15 +0200 Subject: [PATCH 528/821] Add new validation rule `unique_within_resource` This new validation rule enforces the uniqueness of an attribute only at API resource level, contrasting with the `unique` rule that enforces uniqueness at database collection level. --- eve/io/mongo/validation.py | 5 +++++ eve/tests/methods/post.py | 6 ++++++ eve/tests/test_settings.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 417ff4646..a532abce8 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -74,6 +74,11 @@ def _validate_unique_to_user(self, unique, field, value): self._is_value_unique(unique, field, value, query) + def _validate_unique_within_resource(self, unique, field, value): + """ {'type': 'boolean'} """ + _, filter_, _, _ = app.data.datasource(self.resource) + self._is_value_unique(unique, field, value, filter_) + def _validate_unique(self, unique, field, value): """ {'type': 'boolean'} """ self._is_value_unique(unique, field, value, {}) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 412a967c6..53843ecdb 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -975,6 +975,12 @@ def test_post_projection_is_honored(self): self.assertTrue("ref" in r) self.assertTrue("aninteger" not in r) + def test_unique_value_different_resources(self): + r, status = self.post("tenant_a", data={"name": "John"}) + self.assert201(status) + r, status = self.post("tenant_b", data={"name": "John"}) + self.assert201(status) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index fcf75d257..3bffd3438 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -282,6 +282,22 @@ }, } +tenant_a = { + "datasource": {"source": "tenants", "filter": {"_tenant": "tenant_a"}}, + "schema": { + "_tenant": {"type": "string", "readonly": True, "default": "tenant_a"}, + "name": {"type": "string", "required": True, "unique_within_resource": True}, + }, +} + +tenant_b = { + "datasource": {"source": "tenants", "filter": {"_tenant": "tenant_b"}}, + "schema": { + "_tenant": {"type": "string", "readonly": True, "default": "tenant_b"}, + "name": {"type": "string", "required": True, "unique_within_resource": True}, + }, +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -316,4 +332,6 @@ "child_products": child_products, "exclusion": exclusion, "test_patch": test_patch, + "tenant_a": tenant_a, + "tenant_b": tenant_b, } From 2cb73522c0a815c80eca82fac4ced70eb28be882 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Mon, 1 Jul 2019 00:40:50 +0200 Subject: [PATCH 529/821] Add config docs entrance to `unique_within_resource` --- docs/config.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 60c4de08a..4423d739e 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1293,6 +1293,17 @@ defining the field validation rules. Allowed validation rules are: If URRA is not active on the endpoint, this rule behaves like ``unique`` +``unique_within_resource`` The value of the field must be unique within + the resource. + + This differs from the ``unique`` rule in that + it will use the datasource filter when searching + for documents with the same value for the field. + Use this when the resource shares the database + collection with other resources but their documents + should not be taken into account when evaluating + the uniqueness of the field. + ``data_relation`` Allows to specify a referential integrity rule that the value must satisfy in order to validate. It is a dict with four keys: From 56f154e015184ad07068090bc0d4f92b1d99331a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 15:44:43 +0200 Subject: [PATCH 530/821] Changelog for #1292 --- CHANGES.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 35ed4a846..4cbb4df27 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +New +~~~ +- ``unique_within_resource`` validation rule. Enforces the uniqueness of an + attribute only at API resource level, contrasting with the ``unique`` rule + that enforces uniqueness at database collection level (`#1291`_) + +.. _`#1291`: https://github.com/pyeve/eve/issues/1291 Version 0.9.2 ------------- From 0c5fdb30e00bddf337ac40952b7419c5f8b03a46 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 15:48:05 +0200 Subject: [PATCH 531/821] Bump version to 0.9.3.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index f9f9d9b34..e57937ec7 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9.2" +__version__ = "0.9.3.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 6d188a864babb9d745acc34f2e575ae844ef70af Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 15:58:20 +0200 Subject: [PATCH 532/821] Full release number on the frontpage --- CHANGES.rst | 4 ++++ docs/index.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4cbb4df27..9442d5037 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,10 @@ New .. _`#1291`: https://github.com/pyeve/eve/issues/1291 +Fixed +~~~~~ +- Display the full release number on Eve frontpage. + Version 0.9.2 ------------- diff --git a/docs/index.rst b/docs/index.rst index 6ce13b2ed..95b5054c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Eve. The Simple Way to REST =========================== -Version |version|. +Version |release|. .. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square :target: https://pypi.org/project/eve From 8fd9bbf99b37d8001a5dbb0ca475df56b669a275 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 16:34:05 +0200 Subject: [PATCH 533/821] Fix: Flask 1.1.1 causes logging test to break Closes #1296 --- CHANGES.rst | 3 +++ eve/tests/logging.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9442d5037..60aedd030 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,9 @@ New Fixed ~~~~~ - Display the full release number on Eve frontpage. +- Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) + +.. _`#1296`: https://github.com/pyeve/eve/issues/1296 Version 0.9.2 ------------- diff --git a/eve/tests/logging.py b/eve/tests/logging.py index fd32fffd2..59442cd69 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -12,7 +12,7 @@ class TestUtils(TestBase): def test_logging_info(self, l): self.app.logger.propagate = True self.app.logger.info("test info") - l.check(("flask.app", "INFO", "test info")) + l.check(("eve", "INFO", "test info")) log_record = l.records[0] self.assertEqual(log_record.clientip, None) From 483d66eba2d40e7a8a0831a8e709e6034615b314 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 16:39:05 +0200 Subject: [PATCH 534/821] Fix: documentation typo. Closes #1293. --- CHANGES.rst | 4 +++- docs/config.rst | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 60aedd030..c96fdc599 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,10 +16,12 @@ New Fixed ~~~~~ -- Display the full release number on Eve frontpage. +- Documentation typo (`#1293`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) +- Display the full release number on Eve frontpage. .. _`#1296`: https://github.com/pyeve/eve/issues/1296 +.. _`#1293`: https://github.com/pyeve/eve/issues/1293 Version 0.9.2 ------------- diff --git a/docs/config.rst b/docs/config.rst index 4423d739e..6be23927d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1124,7 +1124,7 @@ always lowercase. with the default Mongo layer, setting this to ``False`` will result in an error. Defaults to ``True``. -``normalze_on_patch`` If ``True``, the patch document will be +``normalize_on_patch`` If ``True``, the patch document will be normalized according to schema. This means if a field is not included in the patch body, it will be reset to the default value in its From c0bfb497c3134864f72ff5209a1a8c99367b2d9b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 17:08:47 +0200 Subject: [PATCH 535/821] Drop support for Python 3.4 Closes #1297 --- .travis.yml | 1 - CHANGES.rst | 2 ++ CONTRIBUTING.rst | 2 +- docs/index.rst | 2 +- setup.py | 2 +- tox.ini | 3 +-- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f9daa001..722220fe8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ cache: pip script: tox --recreate python: - 2.7 - - 3.4 - 3.5 - 3.6 - 3.7 diff --git a/CHANGES.rst b/CHANGES.rst index c96fdc599..afa785f39 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,10 +8,12 @@ In Development New ~~~ +- Drop support for Python 3.4 (`#1297`_) - ``unique_within_resource`` validation rule. Enforces the uniqueness of an attribute only at API resource level, contrasting with the ``unique`` rule that enforces uniqueness at database collection level (`#1291`_) +.. _`#1297`: https://github.com/pyeve/eve/issues/1297 .. _`#1291`: https://github.com/pyeve/eve/issues/1291 Fixed diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2aea40ec8..d52314826 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -131,7 +131,7 @@ Or to only run tests in a particular test module on Python 3.6:: Travis-CI will run the full suite when you submit your pull request. The full test suite takes a long time to run because it tests multiple combinations of -Python and dependencies. You need to have Python 2.7, 3.4, 3.5, 3.6, and PyPy +Python and dependencies. You need to have Python 2.7, 3.5, 3.6, and PyPy installed to run all of the environments. Then run:: tox diff --git a/docs/index.rst b/docs/index.rst index 95b5054c0..4c93bab8e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.7, 3.4+, and PyPy. +The codebase is thoroughly tested under Python 2.7, 3.5+, and PyPy. .. note:: The use of **Python 3** is *highly* preferred over Python 2. Consider upgrading your applications and infrastructure if you find yourself *still* using Python 2 in production today. diff --git a/setup.py b/setup.py index dbc10479b..52816d359 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ test_suite="eve.tests", install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, !=3.4.*", classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", diff --git a/tox.ini b/tox.ini index d8ffa741c..344a61386 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py34,py35,py36,py37,pypy,linting +envlist=py27,py35,py36,py37,pypy,linting [testenv] extras=tests @@ -15,7 +15,6 @@ commands = pre-commit run --all-files [travis] python = 2.7: py27 - 3.4: py34 3.5: py35 3.6: py36 3.7: py37 From eaa03f211b2572f29cdf3332328ae08779559995 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 11 Jul 2019 17:33:21 +0200 Subject: [PATCH 536/821] Bump version to 0.10.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index e57937ec7..a6772549a 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.9.3.dev0" +__version__ = "0.10.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From fa32447d13429dda5cf291dbbf6e6ee6169bfd8d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 5 Aug 2019 10:39:20 +0200 Subject: [PATCH 537/821] Update EveGenie link (new maintainer: David Zisky) --- CHANGES.rst | 1 + docs/extensions.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index afa785f39..3807fe40c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,7 @@ Fixed - Documentation typo (`#1293`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. +- Update link to EveGenie repository. New maintainer: David Zisky. .. _`#1296`: https://github.com/pyeve/eve/issues/1296 .. _`#1293`: https://github.com/pyeve/eve/issues/1293 diff --git a/docs/extensions.rst b/docs/extensions.rst index 5abcd2429..6a07f9d68 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -116,7 +116,7 @@ power our iOS, Web and Windows applications. EveGenie -------- -*by Erin Corson and Matt Tucker* +*by Erin Corson and Matt Tucker, maintained by David Zisky.* EveGenie_ is a tool for generating Eve schemas. It accepts a json document of one or more resources and provides you with a starting schema definition. @@ -146,7 +146,7 @@ Olivier Poitrey, a long time Eve contributor and sustainer. REST Layer is .. _Flask-Sentinel: https://github.com/pyeve/flask-sentinel .. _Eve-Auth-JWT: https://github.com/rs/eve-auth-jwt .. _`REST Layer`: https://github.com/rs/rest-layer -.. _EveGenie: https://github.com/newmediadenver/evegenie +.. _EveGenie: https://github.com/DavidZisky/evegenie .. _Eve-Swagger: https://github.com/pyeve/eve-swagger .. _`Meet Eve-Swagger`: http://nicolaiarocci.com/announcing-eve-swagger/ .. _Eve-Neo4j: https://github.com/Abraxas-Biosystems/eve-neo4j From de548b7fedc2c14702c6240dc4e420aefe5d3b15 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 9 Aug 2019 10:21:22 +0200 Subject: [PATCH 538/821] Fix: MONGO_REPLICA_SET is ignored Closes #1302 --- CHANGES.rst | 2 ++ eve/io/mongo/flask_pymongo.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3807fe40c..938cb0371 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,11 +18,13 @@ New Fixed ~~~~~ +- MONGO_REPLICA_SET ignored (`#1302`_) - Documentation typo (`#1293`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1302`: https://github.com/pyeve/eve/issues/1302 .. _`#1296`: https://github.com/pyeve/eve/issues/1296 .. _`#1293`: https://github.com/pyeve/eve/issues/1293 diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index 6d44626f0..5597a389c 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -55,6 +55,9 @@ def config_to_kwargs(mapping): # w, wtimeout, j and fsync client_kwargs.update(app.config[key("WRITE_CONCERN")]) + if key("REPLICA_SET") in app.config: + client_kwargs["replicaset"] = app.config[key("REPLICA_SET")] + uri_parser.validate_options(client_kwargs) if key("URI") in app.config: From 398c78bc8dfc032d2faca2a4a57bf1040dd63417 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 9 Aug 2019 15:36:47 +0200 Subject: [PATCH 539/821] Fix: Delete resource that is empty returns 404 Closes #1299 --- CHANGES.rst | 3 +++ eve/methods/delete.py | 15 +++++++++------ eve/tests/methods/delete.py | 10 +++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 938cb0371..338b1b361 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,12 +18,15 @@ New Fixed ~~~~~ +- (*breaking*) Delete on empty resource returns 404, should return 204 + (`#1299`_) - MONGO_REPLICA_SET ignored (`#1302`_) - Documentation typo (`#1293`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1299`: https://github.com/pyeve/eve/issues/1299 .. _`#1302`: https://github.com/pyeve/eve/issues/1302 .. _`#1296`: https://github.com/pyeve/eve/issues/1296 .. _`#1293`: https://github.com/pyeve/eve/issues/1293 diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 8f5c40559..413799a40 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -30,6 +30,10 @@ import copy +def all_done(): + return {}, None, None, 204 + + @ratelimit() @requires_auth("item") @pre_event @@ -102,7 +106,7 @@ def deleteitem_internal( **lookup ) if not original or (soft_delete_enabled and original.get(config.DELETED) is True): - abort(404) + return all_done() # notify callbacks if suppress_callbacks is not True: @@ -182,7 +186,7 @@ def deleteitem_internal( getattr(app, "on_deleted_item")(resource, original) getattr(app, "on_deleted_item_%s" % resource)(original) - return {}, None, None, 204 + return all_done() @requires_auth("resource") @@ -220,7 +224,7 @@ def delete(resource, **lookup): result, _ = app.data.find(resource, default_request, lookup) originals = list(result) if not originals: - abort(404) + return all_done() # I add new callback as I want the framework to be retro-compatible getattr(app, "on_delete_resource_originals")(resource, originals, lookup) getattr(app, "on_delete_resource_originals_%s" % resource)(originals, lookup) @@ -228,12 +232,11 @@ def delete(resource, **lookup): if resource_def["soft_delete"]: # I need to check that I have at least some documents not soft_deleted - # Otherwise, I should abort 404 # I skip all the soft_deleted documents originals = [x for x in originals if x.get(config.DELETED) is not True] if not originals: # Nothing to be deleted - abort(404) + return all_done() for document in originals: lookup[id_field] = document[id_field] deleteitem_internal( @@ -258,4 +261,4 @@ def delete(resource, **lookup): getattr(app, "on_deleted_resource")(resource) getattr(app, "on_deleted_resource_%s" % resource)() - return {}, None, None, 204 + return all_done() diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 424858b43..721056b12 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -143,7 +143,7 @@ def test_delete(self): def test_delete_non_existant(self): url = self.item_id_url[:-5] + "00000" r, status = self.delete(url, headers=self.etag_headers) - self.assert404(status) + self.assert204(status) def test_delete_write_concern(self): # should get a 500 since there's no replicaset on the mongod instance @@ -371,7 +371,7 @@ def soft_delete_item(etag): def test_multiple_softdelete(self): """After an item has been soft deleted, subsequent DELETEs should - return a 404 Not Found response. + return a 204 Not Found response. """ r, status = self.delete(self.item_id_url, headers=self.etag_headers) self.assert204(status) @@ -381,7 +381,7 @@ def test_multiple_softdelete(self): # Second soft DELETE should return 404 Not Found r, status = self.delete(self.item_id_url, headers=[("If-Match", new_etag)]) - self.assert404(status) + self.assert204(status) def test_softdelete_deleted_field(self): """The configured 'deleted' field should be added to all documents to indicate @@ -738,9 +738,9 @@ def filter_this(resource, request, lookup): lookup["_id"] = self.unknown_item_id self.app.on_pre_DELETE += filter_this - # Would normally delete the known document; will return 404 instead. + # Would normally delete the known document; will return 204 instead. r, s = self.parse_response(self.delete_item()) - self.assert404(s) + self.assert204(s) def test_on_post_DELETE_for_item(self): devent = DummyEvent(self.after_delete) From 205d3fae024990cf2b9d9c281dd224361a53579a Mon Sep 17 00:00:00 2001 From: Dominik Kellner Date: Sat, 10 Aug 2019 10:31:40 +0200 Subject: [PATCH 540/821] Update installation instructions - use HTTPS instead of HTTP - correct example command line output - use `pip install .` instead of `python setup.py install` --- docs/install.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 0681675c3..9b5cdbe8b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -5,7 +5,7 @@ Installation This part of the documentation covers the installation of Eve. The first step to using any software package is getting it properly installed. -Installing Eve is simple with `pip `_: +Installing Eve is simple with `pip `_: .. code-block:: console @@ -23,17 +23,20 @@ Get the git checkout in a new virtualenv and run in development mode. .. code-block:: console - $ git clone http://github.com/pyeve/eve.git - Initialized empty Git repository in ~/dev/eve/.git/ + $ git clone https://github.com/pyeve/eve.git + Cloning into 'eve'... + ... $ cd eve $ virtualenv venv - New python executable in venv/bin/python + ... + Installing setuptools, pip, wheel... + done. $ . venv/bin/activate - $ python setup.py install + $ pip install . ... - Finished processing dependencies for Eve + Successfully installed ... This will pull in the dependencies and activate the git head as the current version inside the virtualenv. Then all you have to do is run ``git pull @@ -47,10 +50,8 @@ To just get the development version without git, do this instead: $ cd eve $ virtualenv venv $ . venv/bin/activate - New python executable in venv/bin/python - - $ pip install git+git://github.com/pyeve/eve.git + $ pip install git+https://github.com/pyeve/eve.git ... - Cleaning up... + Successfully installed ... And you're done! From 5fdee8d37fd49c3fcd220aad455aa250ee8856b6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 26 Aug 2019 09:44:57 +0200 Subject: [PATCH 541/821] Changelog for #1303 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 338b1b361..ba7171068 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,14 +18,16 @@ New Fixed ~~~~~ +- Update installation instructions (`#1303`_) - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) -- MONGO_REPLICA_SET ignored (`#1302`_) +- ``MONGO_REPLICA_SET`` ignored (`#1302`_) - Documentation typo (`#1293`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1303`: https://github.com/pyeve/eve/pull/1303 .. _`#1299`: https://github.com/pyeve/eve/issues/1299 .. _`#1302`: https://github.com/pyeve/eve/issues/1302 .. _`#1296`: https://github.com/pyeve/eve/issues/1296 From 0a2bdab033bc58032e9fea2952aa75d6f362b0bb Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 16 Sep 2019 15:29:25 +0100 Subject: [PATCH 542/821] fix projection curl commands (#1298) --- docs/features.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 965a42c89..39cb9bfc9 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -980,7 +980,7 @@ where the client dictates which fields should be returned by the API. .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?projection={"lastname": 1, "born": 1} + $ curl -i -G http://eve-demo.herokuapp.com/people --data-urlencode 'projection={"lastname": 1, "born": 1}' HTTP/1.1 200 OK The query above will only return *lastname* and *born* out of all the fields @@ -988,7 +988,7 @@ available in the 'people' resource. You can also exclude fields: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?projection={"born": 0} + $ curl -i -G http://eve-demo.herokuapp.com/people --data-urlencode 'projection={"born": 0}' HTTP/1.1 200 OK The above will return all fields but *born*. Please note that key fields such From b2f79cf9cb03d8070c4a3aeb3ad8deffb398acae Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Sep 2019 09:39:21 +0200 Subject: [PATCH 543/821] Changelog for #1312 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ba7171068..fe48576a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,7 @@ New Fixed ~~~~~ +- Curl request in projection examples do not work (`#1298`_) - Update installation instructions (`#1303`_) - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) @@ -27,6 +28,7 @@ Fixed - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1298`: https://github.com/pyeve/eve/issues/1298 .. _`#1303`: https://github.com/pyeve/eve/pull/1303 .. _`#1299`: https://github.com/pyeve/eve/issues/1299 .. _`#1302`: https://github.com/pyeve/eve/issues/1302 From 5aa04b6c1acbbe0f2c83b60ccab314f797f3a5b1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Sep 2019 09:39:49 +0200 Subject: [PATCH 544/821] Saurabh Shandilya --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c2ec21a2f..005b94604 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ Patches and Contributions - Sam Luu - Samuel Sutch - Samuli Tuomola +- Saurabh Shandilya - Sebastien Estienne - Sebastián Magrí - Serge Kir From 3e26ba7363ef7508b5ac7ef96b20f06cadc5cd18 Mon Sep 17 00:00:00 2001 From: Tano Abeleyra Date: Fri, 27 Sep 2019 23:48:40 -0300 Subject: [PATCH 545/821] Fix minor typo in documentation --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 6be23927d..90d17ce9d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1059,7 +1059,7 @@ always lowercase. intensive operation. You might therefore want to handle this task manually, out of the context of API instantiation. Also remember - that, by default, any already exsistent index + that, by default, any already existent index for which the definition has been changed, will be dropped and re-created. From efcd44234f0c6027697db7a548679eaabcb3d3a5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 30 Sep 2019 08:38:05 +0200 Subject: [PATCH 546/821] Changelog for #1315 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fe48576a3..c72f6b833 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,11 +23,12 @@ Fixed - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) - ``MONGO_REPLICA_SET`` ignored (`#1302`_) -- Documentation typo (`#1293`_) +- Documentation typo (`#1293`_, `#1315`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1315`: https://github.com/pyeve/eve/pull/1315 .. _`#1298`: https://github.com/pyeve/eve/issues/1298 .. _`#1303`: https://github.com/pyeve/eve/pull/1303 .. _`#1299`: https://github.com/pyeve/eve/issues/1299 From daa3f2e7b765b9212403057bab7504d7973f1e1c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 30 Sep 2019 08:38:37 +0200 Subject: [PATCH 547/821] Tano Abeleyra --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 005b94604..95cee6171 100644 --- a/AUTHORS +++ b/AUTHORS @@ -170,6 +170,7 @@ Patches and Contributions - Stanislav Heller - Stratos Gerakakis - Sybren A. Stüvel +- Tano Abeleyra - Taylor Brown - Thomas Sileo - Tim Jacobi From e0ae65c908fcf41ab3c53d7a50bad758480f6767 Mon Sep 17 00:00:00 2001 From: alexmisk Date: Fri, 25 Oct 2019 13:27:40 +0300 Subject: [PATCH 548/821] Fix typos --- docs/features.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 39cb9bfc9..64d837dd2 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -773,7 +773,7 @@ subdocument. Keep in mind that ``PATCH`` cannot remove a field, but only update existing values. Also, by default ``PATCH`` will normalize missing body fields that -have defautl values defined in the schema. Consider the schema above. If your +have default values defined in the schema. Consider the schema above. If your ``PATCH`` has a body like this: :: @@ -802,7 +802,7 @@ Then the updated document will look like this: } That is, ``contact.phone`` has been reset to its default value. This might -now been the desired behavior. To change it, you can set +not been the desired behavior. To change it, you can set ``normalize_on_patch`` (or ``NORMALIZE_ON_PATCH`` globally) to ``False``. Now the updated document will look like this: From 8920ea2cb7894bbc21c1e61f692519c34a3d264b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Oct 2019 09:10:45 +0100 Subject: [PATCH 549/821] Alex Misk --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 95cee6171..f30cc85d3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Patches and Contributions - Aayush Sarva - Alberto Marin +- Alex Misk - Alexander Dietmüller - Alexander Hendorf - Amedeo Bussi From 97cfc96c768010e44ae08b8e13914b068f736cce Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 27 Oct 2019 09:12:36 +0100 Subject: [PATCH 550/821] Changelog for #1322 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c72f6b833..dba6232ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,11 +23,12 @@ Fixed - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) - ``MONGO_REPLICA_SET`` ignored (`#1302`_) -- Documentation typo (`#1293`_, `#1315`_) +- Documentation typo (`#1293`_, `#1315`_, `#1322`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1322`: https://github.com/pyeve/eve/pull/1322 .. _`#1315`: https://github.com/pyeve/eve/pull/1315 .. _`#1298`: https://github.com/pyeve/eve/issues/1298 .. _`#1303`: https://github.com/pyeve/eve/pull/1303 From 5dcc4f30feae35eb5dc6bd564b0e5188e8f6b567 Mon Sep 17 00:00:00 2001 From: Adam Walsh Date: Fri, 8 Nov 2019 17:33:01 -0500 Subject: [PATCH 551/821] Update README funding URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 07d6f6a82..cf2f9623b 100644 --- a/README.rst +++ b/README.rst @@ -99,4 +99,4 @@ distributed under the `BSD license `_. .. _`Nicola Iarocci`: http://nicolaiarocci.com -.. _`funding page`: http://python-eve.org/funding +.. _`funding page`: http://python-eve.org/funding.html From 246007a910d613d2b4d15ea165098d5e5863f1a5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 Nov 2019 19:19:00 +0100 Subject: [PATCH 552/821] Adam Walsh --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index f30cc85d3..7aa3b943f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` +- Adam Walsh - Aayush Sarva - Alberto Marin - Alex Misk From 538707e99afcb0570221f879ba974d545443d49d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 Nov 2019 19:21:39 +0100 Subject: [PATCH 553/821] Changelog for #1324 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index dba6232ba..2ffca746f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,11 +23,12 @@ Fixed - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) - ``MONGO_REPLICA_SET`` ignored (`#1302`_) -- Documentation typo (`#1293`_, `#1315`_, `#1322`_) +- Documentation typo (`#1293`_, `#1315`_, `#1322`_, `#1324`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1324`: https://github.com/pyeve/eve/pull/1324 .. _`#1322`: https://github.com/pyeve/eve/pull/1322 .. _`#1315`: https://github.com/pyeve/eve/pull/1315 .. _`#1298`: https://github.com/pyeve/eve/issues/1298 From 3985685b1dbcdab8c1025dbb3c98d61e3be4608f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 Nov 2019 19:49:47 +0100 Subject: [PATCH 554/821] linting fix --- .pre-commit-config.yaml | 2 +- eve/methods/common.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a20521c9..9457e4f63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: stable hooks: - id: black - language_version: python3.7 + language_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 hooks: diff --git a/eve/methods/common.py b/eve/methods/common.py index eb17ced70..4b65b2d8d 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -888,9 +888,11 @@ def embedded_document(references, data_relation, field_name): ) embedded_docs.append(embedded_doc) else: - id_value_to_sort, list_of_id_field_name, subresources_query = generate_query_and_sorting_criteria( - data_relation, references - ) + ( + id_value_to_sort, + list_of_id_field_name, + subresources_query, + ) = generate_query_and_sorting_criteria(data_relation, references) for subresource in subresources_query: result, _ = app.data.find( subresource, None, subresources_query[subresource] From c2b12e2564a940928afbd61407c742f0a9d4bed3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 Nov 2019 19:59:20 +0100 Subject: [PATCH 555/821] go back to using py37 on pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9457e4f63..7a20521c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: stable hooks: - id: black - language_version: python3.6 + language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 hooks: From 6ee4ca4db1c91d2ea7cd317be10e6fdb94f0e518 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 12 Nov 2019 17:40:17 +0100 Subject: [PATCH 556/821] Add python 3.8 to test matrix Closes #1326 --- .travis.yml | 1 + CHANGES.rst | 2 ++ tox.ini | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 722220fe8..fc43e1143 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ python: - 3.5 - 3.6 - 3.7 + - 3.8 - pypy3.5-6.0 install: travis_retry pip install tox-travis services: diff --git a/CHANGES.rst b/CHANGES.rst index 2ffca746f..0a9b60245 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,11 +8,13 @@ In Development New ~~~ +- Python 3.8 added to CI matrix (`#1326`_) - Drop support for Python 3.4 (`#1297`_) - ``unique_within_resource`` validation rule. Enforces the uniqueness of an attribute only at API resource level, contrasting with the ``unique`` rule that enforces uniqueness at database collection level (`#1291`_) +.. _`#1326`: https://github.com/pyeve/eve/issues/1326 .. _`#1297`: https://github.com/pyeve/eve/issues/1297 .. _`#1291`: https://github.com/pyeve/eve/issues/1291 diff --git a/tox.ini b/tox.ini index 344a61386..a5e3611ee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py35,py36,py37,pypy,linting +envlist=py27,py35,py36,py37,py38,pypy,linting [testenv] extras=tests @@ -18,6 +18,7 @@ python = 3.5: py35 3.6: py36 3.7: py37 + 3.8: py38 pypy: pypy [flake8] From aa1ddedfc7cbd8f66300ffae42a44dc0fa07c1dd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 12 Nov 2019 17:52:07 +0100 Subject: [PATCH 557/821] Fix werkzeug's crash on Python 3.8 Closes #1325 --- CHANGES.rst | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0a9b60245..ec56d8b06 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,7 @@ New Fixed ~~~~~ +- Werkzeug 0.15.4 crashes with Python 3.8 (`#1325`_) - Curl request in projection examples do not work (`#1298`_) - Update installation instructions (`#1303`_) - (*breaking*) Delete on empty resource returns 404, should return 204 @@ -30,6 +31,7 @@ Fixed - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1325`: https://github.com/pyeve/eve/pull/1325 .. _`#1324`: https://github.com/pyeve/eve/pull/1324 .. _`#1322`: https://github.com/pyeve/eve/pull/1322 .. _`#1315`: https://github.com/pyeve/eve/pull/1315 diff --git a/setup.py b/setup.py index 52816d359..38222da20 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "flask>=1.0", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", - "werkzeug==0.15.4", + "werkzeug==0.15.5", ] EXTRAS_REQUIRE = { From beb8fa1c84a0e646cb9440e9aceed04764c255f8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Nov 2019 08:56:20 +0100 Subject: [PATCH 558/821] drop CI stages and streamline both tox.ini and travis.yml --- .travis.yml | 43 ++++++++++++++++++------------------------- pytest.ini | 3 +++ tox.ini | 13 ++----------- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc43e1143..c7569496a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,28 @@ dist: xenial -sudo: false language: python -stages: - - linting - - test cache: pip -script: tox --recreate -python: - - 2.7 - - 3.5 - - 3.6 - - 3.7 - - 3.8 - - pypy3.5-6.0 -install: travis_retry pip install tox-travis services: - mongodb - redis-server before_script: - sleep 15 - mongo eve_test --eval 'db.createUser({user:"test_user",pwd:"test_pw",roles:["readWrite"]});' +install: travis_retry pip install tox-travis +script: tox --recreate -jobs: - include: - - stage: linting - python: '3.7' - env: - install: - - pip install pre-commit - - pre-commit install-hooks - before_script: - services: - script: - - pre-commit run --all-files +matrix: + include: + - env: TOXENV=linting + python: "3.7" + - env: TOXENV=py27 + python: "2.7" + - env: TOXENV=py35 + python: "3.5" + - env: TOXENV=py36 + python: "3.6" + - env: TOXENV=py37 + python: "3.7" + - env: TOXENV=py38 + python: "3.8" + - env: TOXENV=pypy3 + python: "pypy3.5-6.0" diff --git a/pytest.ini b/pytest.ini index bbd1d9ba4..60a49bc66 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,6 @@ testpaths=eve/tests python_files=eve/tests/*.py addopts = --maxfail=2 -rf --capture=no norecursedirs = testsuite .tox +filterwarnings = + ignore :: DeprecationWarning + ignore :: PendingDeprecationWarning diff --git a/tox.ini b/tox.ini index a5e3611ee..62f54eb78 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py35,py36,py37,py38,pypy,linting +envlist=py27,py35,py36,py37,py38,pypy3,linting [testenv] extras=tests @@ -8,19 +8,10 @@ commands=py.test eve {posargs} [testenv:linting] skipsdist = True usedevelop = True -basepython = python3.6 +basepython = python3.7 deps = pre-commit commands = pre-commit run --all-files -[travis] -python = - 2.7: py27 - 3.5: py35 - 3.6: py36 - 3.7: py37 - 3.8: py38 - pypy: pypy - [flake8] max-line-length = 88 ignore = E401,E722,W503,F821,E501,E203 From e533a1932030167f465dfe13f99fc0b79b823c57 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Nov 2019 10:37:35 +0100 Subject: [PATCH 559/821] Add Python 3.8 to the list of trove classifiers Addresses #1326 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 38222da20..fb52dae6b 100755 --- a/setup.py +++ b/setup.py @@ -60,10 +60,10 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", From d260687b2eaee2128044b4e4b9caccef772a2738 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Fri, 22 Nov 2019 02:02:55 +0000 Subject: [PATCH 560/821] Fixes small type in CONTRIBUTING.rst --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d52314826..a8a5d7c2e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -137,7 +137,7 @@ installed to run all of the environments. Then run:: tox Please note that you need an active MongoDB instance running on localhost in -order for the tests run. Also, be advived that in order to execute the +order for the tests run. Also, be advised that in order to execute the :ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting tests are silently skipped if any of the two conditions are not met. From 79e6deb3467c24b4119db5076b0d7a30b5f4e2eb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 23 Nov 2019 11:38:59 +0100 Subject: [PATCH 561/821] Chagelog for #1327 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ec56d8b06..38345b623 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,11 +26,12 @@ Fixed - (*breaking*) Delete on empty resource returns 404, should return 204 (`#1299`_) - ``MONGO_REPLICA_SET`` ignored (`#1302`_) -- Documentation typo (`#1293`_, `#1315`_, `#1322`_, `#1324`_) +- Documentation typo (`#1293`_, `#1315`_, `#1322`_, `#1324`_, `#1327`_) - Flask 1.1.1 breaks ``test_logging_info`` test (`#1296`_) - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1327`: https://github.com/pyeve/eve/pull/1327 .. _`#1325`: https://github.com/pyeve/eve/pull/1325 .. _`#1324`: https://github.com/pyeve/eve/pull/1324 .. _`#1322`: https://github.com/pyeve/eve/pull/1322 From 253b7d7ce5483da5c84a51cb16634a54c693f948 Mon Sep 17 00:00:00 2001 From: Stefaan Ghysels Date: Tue, 26 Nov 2019 16:18:43 +0100 Subject: [PATCH 562/821] Fix assert401or405 --- eve/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 7d47059e0..b73b45c72 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -339,7 +339,7 @@ def assert401(self, status): self.assertEqual(status, 401) def assert401or405(self, status): - self.assertTrue(status == 401 or 405) + self.assertTrue(status in [401, 405]) def assert403(self, status): self.assertEqual(status, 403) From 295564774911c2c6619371c907f71c2ba095362c Mon Sep 17 00:00:00 2001 From: Stefaan Ghysels Date: Tue, 26 Nov 2019 16:11:00 +0100 Subject: [PATCH 563/821] Don't assume test_user exists --- eve/tests/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index b73b45c72..bfcb1e9c5 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -364,7 +364,9 @@ def setupDB(self): self.connection.drop_database(MONGO_DBNAME) if MONGO_USERNAME: db = self.connection[MONGO_DBNAME] - db.command("dropUser", MONGO_USERNAME) + info = db.command("usersInfo", MONGO_USERNAME) + if any(user["user"] == MONGO_USERNAME for user in info["users"]): + db.command("dropUser", MONGO_USERNAME) db.command( "createUser", MONGO_USERNAME, pwd=MONGO_PASSWORD, roles=["dbAdmin"] ) From 8816e2cbe7e44b4c50978318297aa87a4f1acbd1 Mon Sep 17 00:00:00 2001 From: Stefaan Ghysels Date: Tue, 26 Nov 2019 16:09:51 +0100 Subject: [PATCH 564/821] Minor style fixes --- eve/flaskapp.py | 15 ++++++--------- eve/methods/delete.py | 8 ++++---- eve/methods/get.py | 15 +++++++-------- eve/methods/post.py | 2 +- eve/methods/put.py | 2 +- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 67c0effb6..cf07ccd0e 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -383,7 +383,7 @@ def _validate_resource_settings(self, resource, settings): "POST" in settings["resource_methods"] or "PATCH" in settings["item_methods"] ): - if len(settings["schema"]) == 0: + if not settings["schema"]: raise ConfigException( "A resource schema must be provided " "when POST or PATCH methods are allowed " @@ -494,10 +494,7 @@ def validate_field_name(field): if resource_settings["soft_delete"] is True: fields += [self.config["DELETED"]] - offenders = [] - for field in fields: - if field in schema: - offenders.append(field) + offenders = [field for field in fields if field in schema] if offenders: raise SchemaException( 'field(s) "%s" not allowed in "%s" schema ' @@ -511,8 +508,8 @@ def validate_field_name(field): for field, ruleset in schema.items(): validate_field_name(field) if isinstance(ruleset, dict) and "dict" in ruleset.get("type", ""): - for field in ruleset.get("schema", {}).keys(): - validate_field_name(field) + for field_ in ruleset.get("schema", {}): + validate_field_name(field_) # check data_relation rules if "data_relation" in ruleset: @@ -728,7 +725,7 @@ def _set_resource_projection(self, ds, schema, settings): # If inclusion projections are defined, exclusion projections are # just ignored. # Enhance the projection with automatic fields. - if len(schema) and settings["allow_unknown"] is False: + if schema and settings["allow_unknown"] is False: inclusion_projection = dict( [(k, v) for k, v in projection.items() if v == 1] ) @@ -740,7 +737,7 @@ def _set_resource_projection(self, ds, schema, settings): projection.update( dict( (field, 1) - for (field) in schema + for field in schema if field not in exclusion_projection ) ) diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 413799a40..36bfd5819 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -109,7 +109,7 @@ def deleteitem_internal( return all_done() # notify callbacks - if suppress_callbacks is not True: + if not suppress_callbacks: getattr(app, "on_delete_item")(resource, original) getattr(app, "on_delete_item_%s" % resource)(original) @@ -151,7 +151,7 @@ def deleteitem_internal( # document might miss one or more media fields because of datasource # and/or client projection. missing_media_fields = [f for f in media_fields if f not in original] - if len(missing_media_fields): + if missing_media_fields: # retrieve the whole document so we have all media fields available # Should be very a rare occurrence. We can't get rid of the # get_document() call since it also deals with etag matching, which @@ -182,7 +182,7 @@ def deleteitem_internal( # update oplog if needed oplog_push(resource, original, "DELETE", id) - if suppress_callbacks is not True: + if not suppress_callbacks: getattr(app, "on_deleted_item")(resource, original) getattr(app, "on_deleted_item_%s" % resource)(original) @@ -233,7 +233,7 @@ def delete(resource, **lookup): if resource_def["soft_delete"]: # I need to check that I have at least some documents not soft_deleted # I skip all the soft_deleted documents - originals = [x for x in originals if x.get(config.DELETED) is not True] + originals = [x for x in originals if not x.get(config.DELETED)] if not originals: # Nothing to be deleted return all_done() diff --git a/eve/methods/get.py b/eve/methods/get.py index 708b360b8..82ab48896 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -164,12 +164,11 @@ def prune_aggregation_stage(d): For example, we have endpoint with a stage like {'$lookup': {'$userId': '$a', '$name': '$b'}}, $a is provided but $b is provided as {}. Then the stage will be pruned as {'$lookup': {'$userId': '$a'}} """ - items = [(st_key, st_value) for st_key, st_value in d.items()] - for (st_key, st_value) in items: + for st_key, st_value in list(d.items()): if isinstance(st_value, dict): prune_aggregation_stage(st_value) - if len(st_value.keys()) == 0: - # remove the key: value when value is an empty dict + if not st_value: + # value is an empty dict, remove the key del d[st_key] response = {} @@ -184,7 +183,7 @@ def prune_aggregation_stage(d): abort(400, description="Aggregation query could not be parsed.") for key, value in query.items(): - if key[0] != "$": + if key.startswith("$"): pass for stage in req_pipeline: parse_aggregation_stage(stage, key, value) @@ -193,7 +192,7 @@ def prune_aggregation_stage(d): req_pipeline_pruned = [] for stage in req_pipeline: prune_aggregation_stage(stage) - if len(stage.keys()) > 0: + if stage: req_pipeline_pruned.append(stage) paginated_results = [] @@ -445,7 +444,7 @@ def getitem_internal(resource, **lookup): if (cache_validators[True] > 0) and (cache_validators[False] == 0): return {}, last_modified, etag, 304 - if version == "all" or version == "diffs": + if version in ("all", "diffs"): # find all versions lookup[versioned_id_field(resource_def)] = lookup[resource_def["id_field"]] del lookup[resource_def["id_field"]] @@ -528,7 +527,7 @@ def getitem_internal(resource, **lookup): # the functions modify the document, last_modified and etag # won't be updated to reflect the changes (they always reflect the # documents state on the database). - if resource_def["versioning"] is True and version in ["all", "diffs"]: + if resource_def["versioning"] is True and version in ("all", "diffs"): versions = response if config.DOMAIN[resource]["hateoas"]: versions = response[config.ITEMS] diff --git a/eve/methods/post.py b/eve/methods/post.py index 531ee0aef..0c37c4c2c 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -229,7 +229,7 @@ def post_internal(resource, payl=None, skip_validation=False): app.logger.exception(e) doc_issues["exception"] = str(e) - if len(doc_issues): + if doc_issues: document = {config.STATUS: config.STATUS_ERR, config.ISSUES: doc_issues} failures += 1 diff --git a/eve/methods/put.py b/eve/methods/put.py index 8a522a864..b2dcd44d4 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -247,7 +247,7 @@ def put_internal( app.logger.exception(e) abort(400, description=debug_error_message("An exception occurred: %s" % e)) - if len(issues): + if issues: response[config.ISSUES] = issues response[config.STATUS] = config.STATUS_ERR status = config.VALIDATION_ERROR_STATUS From aa9ccb569467b335781ec31bd72aeb554e869bdc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 29 Nov 2019 14:47:00 +0100 Subject: [PATCH 565/821] Changelog for #1330 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 38345b623..38d78a3e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,7 @@ New Fixed ~~~~~ +- Minor style improvements and 2 test fixes (`#1330`_) - Werkzeug 0.15.4 crashes with Python 3.8 (`#1325`_) - Curl request in projection examples do not work (`#1298`_) - Update installation instructions (`#1303`_) @@ -31,6 +32,7 @@ Fixed - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1330`: https://github.com/pyeve/eve/pull/1330 .. _`#1327`: https://github.com/pyeve/eve/pull/1327 .. _`#1325`: https://github.com/pyeve/eve/pull/1325 .. _`#1324`: https://github.com/pyeve/eve/pull/1324 From f3fa543c86526b75ab0377a3ed23887a1745ceb8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 29 Nov 2019 14:47:18 +0100 Subject: [PATCH 566/821] Stefaan Ghysels --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 7aa3b943f..e2afce16c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,8 +9,8 @@ Development Lead Patches and Contributions ````````````````````````` -- Adam Walsh - Aayush Sarva +- Adam Walsh - Alberto Marin - Alex Misk - Alexander Dietmüller @@ -170,6 +170,7 @@ Patches and Contributions - Sobolev Nikita - Stanislav Filin - Stanislav Heller +- Stefaan Ghysels - Stratos Gerakakis - Sybren A. Stüvel - Tano Abeleyra From f52abf0d35e36828d571f14977a596d0c4d98984 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 09:30:34 +0100 Subject: [PATCH 567/821] Fix: 500 on PATCH/PUT w/Mongo4.2 and _id included Closes #1341 --- CHANGES.rst | 3 +++ eve/io/mongo/mongo.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 38d78a3e9..de6fa05fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,8 @@ New Fixed ~~~~~ +- 500 error when PATCH or PUT are performed on Mongo 4.2 and `_id` is + included with payload (`#1341`_) - Minor style improvements and 2 test fixes (`#1330`_) - Werkzeug 0.15.4 crashes with Python 3.8 (`#1325`_) - Curl request in projection examples do not work (`#1298`_) @@ -32,6 +34,7 @@ Fixed - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1341`: https://github.com/pyeve/eve/issues/1341 .. _`#1330`: https://github.com/pyeve/eve/pull/1330 .. _`#1327`: https://github.com/pyeve/eve/pull/1327 .. _`#1325`: https://github.com/pyeve/eve/pull/1325 diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index ee05579c7..300a5ca44 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -533,9 +533,9 @@ def _change_request(self, resource, id_, changes, original, replace=False): except (pymongo.errors.WriteError, pymongo.errors.OperationFailure) as e: # server error codes and messages changed between 2.4 and 2.6/3.0. server_version = self.driver.db.client.server_info()["version"][:3] - if (server_version == "2.4" and e.code in (13596, 10148)) or ( - server_version in ("2.6", "3.0", "3.2", "3.4", "3.6", "4.0") - and e.code in (66, 16837) + if (server_version == "2.4" and e.code in (13596, 10148)) or e.code in ( + 66, + 16837, ): # attempt to update an immutable field. this usually # happens when a PATCH or PUT includes a mismatching ID_FIELD. From 1bc34894363cf5c590b4d5e021311092163b73c4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 10:04:03 +0100 Subject: [PATCH 568/821] Pin to Cerberus < 2.0 Next year, we are likely to see a Cerberus 2 release, which brings a number of breaking changes. Also, werkzeug API seems relatively stable now. I removed it from the pins (it will come down with Flask.) Closes #1342 --- CHANGES.rst | 4 +++- setup.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index de6fa05fb..fd618f51f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,8 @@ New Fixed ~~~~~ -- 500 error when PATCH or PUT are performed on Mongo 4.2 and `_id` is +- Pin to Cerberus < 2.0 (`#1342`_) +- 500 error when PATCH or PUT are performed on Mongo 4.2 and ``_id`` is included with payload (`#1341`_) - Minor style improvements and 2 test fixes (`#1330`_) - Werkzeug 0.15.4 crashes with Python 3.8 (`#1325`_) @@ -34,6 +35,7 @@ Fixed - Display the full release number on Eve frontpage. - Update link to EveGenie repository. New maintainer: David Zisky. +.. _`#1342`: https://github.com/pyeve/eve/issues/1342 .. _`#1341`: https://github.com/pyeve/eve/issues/1341 .. _`#1330`: https://github.com/pyeve/eve/pull/1330 .. _`#1327`: https://github.com/pyeve/eve/pull/1327 diff --git a/setup.py b/setup.py index fb52dae6b..7d466890f 100755 --- a/setup.py +++ b/setup.py @@ -13,12 +13,11 @@ VERSION = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1) INSTALL_REQUIRES = [ - "cerberus>=1.1", + "cerberus>=1.1,<2.0", "events>=0.3,<0.4", "flask>=1.0", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", - "werkzeug==0.15.5", ] EXTRAS_REQUIRE = { From 62d01fbe5a1b3fbf02385ca706859ac152eb9e9d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 11:21:58 +0100 Subject: [PATCH 569/821] Update changelog title --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fd618f51f..b613b1e28 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ -Eve Changelog -============= +Changelog +========= Here you can see the full list of changes between each Eve release. From 305ba8e16947f2044b730f0ef7f229444edccfaf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 11:52:53 +0100 Subject: [PATCH 570/821] Add a FUNDING.yml file --- FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 FUNDING.yml diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 000000000..94654c589 --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1 @@ +patreon: nicolaiarocci From b76aa7ac9ceb3f0985f673a867c0e96a4576edca Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 16:18:35 +0100 Subject: [PATCH 571/821] Add doc8 to dev requirements Closes #1343 --- CHANGES.rst | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b613b1e28..3e979a39b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,9 @@ New - ``unique_within_resource`` validation rule. Enforces the uniqueness of an attribute only at API resource level, contrasting with the ``unique`` rule that enforces uniqueness at database collection level (`#1291`_) +- Add doc8 to dev-requirements (`#1343`_) +.. _`#1343`: https://github.com/pyeve/eve/issues/1343 .. _`#1326`: https://github.com/pyeve/eve/issues/1326 .. _`#1297`: https://github.com/pyeve/eve/issues/1297 .. _`#1291`: https://github.com/pyeve/eve/issues/1291 diff --git a/setup.py b/setup.py index 7d466890f..8c63ce413 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ ] EXTRAS_REQUIRE = { - "docs": ["sphinx", "alabaster"], + "docs": ["sphinx", "alabaster", "doc8"], "tests": ["redis", "testfixtures", "pytest", "tox"], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] From e246c216d900c48da7461e24dd99b71783d09273 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 18 Dec 2019 08:35:33 +0100 Subject: [PATCH 572/821] Bump version to 1.0 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3e979a39b..de2e02932 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones + +Version 1.0 +----------- + +Released on December 19, 2019. + New ~~~ - Python 3.8 added to CI matrix (`#1326`_) diff --git a/eve/__init__.py b/eve/__init__.py index a6772549a..4d97d8faa 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "0.10.dev0" +__version__ = "1.0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/setup.py b/setup.py index 8c63ce413..9f5e8f71b 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ extras_require=EXTRAS_REQUIRE, python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, !=3.4.*", classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", From f719b0ccb6e0e50b85632ce2c8e5bb9ee6011c31 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 20 Dec 2019 09:08:05 +0100 Subject: [PATCH 573/821] Add GitHub Sponsors to FUNDING.yml --- FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/FUNDING.yml b/FUNDING.yml index 94654c589..ddfcec6bf 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1 +1,2 @@ +github: nicolaiarocci patreon: nicolaiarocci From 3da4b75e353acab87143ed9dcf8937e5abb6f7b8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Jan 2020 10:23:26 +0100 Subject: [PATCH 574/821] Add Jon Kelled and (long time due) Gabriel Wainer to backers list --- docs/funding.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/funding.rst b/docs/funding.rst index cf3303a1a..bf8ed6803 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -48,8 +48,15 @@ Just `get in touch`_ with me. .. _`get in touch`: mailto:nicola@nicolaiarocci.com .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course +Backers +~~~~~~~ +Backers who actively support Eve and Cerberus development: + +- Gabriel Wainer +- Jon Kelled + Generous Backers ----------------- +~~~~~~~~~~~~~~~~ Generous backers who actively support Eve and Cerberus development: .. image:: _static/backers/blokt.png From 2e06ebfdc1fed338fc25a7fbf63dde98914dd11a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Jan 2020 10:32:46 +0100 Subject: [PATCH 575/821] Add GitHub Sponsors to docs --- docs/funding.rst | 5 +++-- docs/index.rst | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/funding.rst b/docs/funding.rst index bf8ed6803..e11ec1cc4 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -26,9 +26,10 @@ donating as a sign of appreciation - like buying me coffee once in a while :) Support Eve development ----------------------- -You can support Eve development by pledging on Patreon or donating on PayPal. +You can support Eve development by pledging on GitHub, Patreon, or donating on PayPal. -- `Become a Backer `_ (recurring pledge) +- `Become a Backer on GitHub `_ +- `Become a Backer on Patreon `_ - `Donate via PayPal `_ (one time) Eve Course at TalkPython Training diff --git a/docs/index.rst b/docs/index.rst index 4c93bab8e..2df8b908b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,7 +69,7 @@ also welcome to make either a recurring pledge or a one time donation if Eve has helped you in your work or personal projects. Every single sign-up makes a significant impact towards making Eve possible. -To join the backer ranks, check out `Eve campaign on Patreon`_. +To join the backer ranks, check out :doc:`the funding page `. .. _demo: @@ -124,4 +124,3 @@ is also a simple `client app`_ available. .. _Cerberus: http://python-cerberus.org .. _events: https://github.com/pyeve/events .. _extensions: http://python-eve.org/extensions -.. _`Eve campaign on Patreon`: https://www.patreon.com/nicolaiarocci From b410905caa13555e57958657122bc4d8e865575b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Jan 2020 10:38:09 +0100 Subject: [PATCH 576/821] Links to sponsorship options directly on homepage For increased visibility --- docs/index.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 2df8b908b..85ef036c5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,7 +69,11 @@ also welcome to make either a recurring pledge or a one time donation if Eve has helped you in your work or personal projects. Every single sign-up makes a significant impact towards making Eve possible. -To join the backer ranks, check out :doc:`the funding page `. +You can support Eve development by pledging on GitHub, Patreon, or donating on PayPal. + +- `Become a Backer on GitHub `_ +- `Become a Backer on Patreon `_ +- `Donate via PayPal `_ (one time) .. _demo: From 4f3f7087f7bc8ab440f2583bb85d608024793878 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Jan 2020 10:41:01 +0100 Subject: [PATCH 577/821] typos --- docs/funding.rst | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/funding.rst b/docs/funding.rst index e11ec1cc4..a5a1e98e2 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -26,7 +26,7 @@ donating as a sign of appreciation - like buying me coffee once in a while :) Support Eve development ----------------------- -You can support Eve development by pledging on GitHub, Patreon, or donating on PayPal. +You can support Eve development by pledging on GitHub, Patreon, or PayPal. - `Become a Backer on GitHub `_ - `Become a Backer on Patreon `_ diff --git a/docs/index.rst b/docs/index.rst index 85ef036c5..9a404b18b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,7 +69,7 @@ also welcome to make either a recurring pledge or a one time donation if Eve has helped you in your work or personal projects. Every single sign-up makes a significant impact towards making Eve possible. -You can support Eve development by pledging on GitHub, Patreon, or donating on PayPal. +You can support Eve development by pledging on GitHub, Patreon, or PayPal. - `Become a Backer on GitHub `_ - `Become a Backer on Patreon `_ From b8d8fcd5a6d2093ec135a021f5b0f48d2c419dfe Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Tue, 14 Jan 2020 00:33:02 +0100 Subject: [PATCH 578/821] Always coerce id fields to objectid even if query_objectid_as_string is true. --- eve/io/mongo/mongo.py | 56 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 300a5ca44..ca8f6351d 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -36,6 +36,7 @@ str_to_date, str_type, ) +from ...versioning import versioned_id_field class MongoJSONEncoder(BaseJSONEncoder): @@ -783,7 +784,7 @@ def is_empty(self, resource): ), ) - def _mongotize(self, source, resource): + def _mongotize(self, source, resource, parse_objectid=True): """ Recursively iterates a JSON dictionary, turning RFC-1123 strings into datetime values and ObjectId-link strings into ObjectIds. @@ -803,14 +804,18 @@ def _mongotize(self, source, resource): .. versionadded:: 0.0.4 """ - schema = config.DOMAIN[resource] - skip_objectid = schema.get("query_objectid_as_string", False) + resource_def = config.DOMAIN[resource] + id_field = resource_def["id_field"] + id_field_versioned = versioned_id_field(resource_def) + skip_objectid = resource_def.get("query_objectid_as_string", False) - def try_cast(v): + def try_cast(k, v, parse_objectid): try: return datetime.strptime(v, config.DATE_FORMAT) except: - if not skip_objectid: + if k in (id_field, id_field_versioned) or ( + parse_objectid and not skip_objectid + ): try: # Convert to unicode because ObjectId() interprets # 12-character strings (but not unicode) as binary @@ -827,17 +832,50 @@ def try_cast(v): else: return v + def get_schema_type(keys, schema): + def dict_sub_schema(base): + if base.get("type") == "dict": + return base.get("schema") + return base + + if not isinstance(schema, dict): + return None + if not keys: + return schema.get("type") + + k = keys[0] + keys = keys[1:] + schema_type = schema[k].get("type") if k in schema else None + if schema_type == "list": + # TODO: do we need to check for accounts[0] syntax here? if so we need to + if "items" in schema[k]: + items = schema[k].get("items") or [] + possible_types = [get_schema_type(keys, item) for item in items] + if "objectid" in possible_types: + return "objectid" + else: + return next((t for t in possible_types if t), None) + elif "schema" in schema[k]: + # recursively check the schema + return get_schema_type( + keys, dict_sub_schema(schema[k].get("schema")) + ) + elif schema_type == "dict": + return get_schema_type(keys, dict_sub_schema(schema[k].get("schema"))) + else: + return schema_type + for k, v in source.items(): if isinstance(v, dict): - self._mongotize(v, resource) + self._mongotize(v, resource, parse_objectid) # was False elif isinstance(v, list): for i, v1 in enumerate(v): if isinstance(v1, dict): - source[k][i] = self._mongotize(v1, resource) + source[k][i] = self._mongotize(v1, resource, parse_objectid) # was False else: - source[k][i] = try_cast(v1) + source[k][i] = try_cast(k, v1, parse_objectid) elif isinstance(v, str_type): - source[k] = try_cast(v) + source[k] = try_cast(k, v, parse_objectid) return source From 73c6683600e514210da6c4971844ccd6fe2101f0 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Thu, 16 Jan 2020 10:42:07 +0100 Subject: [PATCH 579/821] An attempt at a failing unit test. --- eve/tests/methods/get.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index e89fc32fc..0ce749dfe 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -240,6 +240,19 @@ def test_get_where_mongo_objectid_as_string(self): resource = response["_items"] self.assertEqual(len(resource), 0) + def test_get_where_mongo_objectid_as_string_but_field_is_id(self): + where_in = '{"tid": { "$in": ["%s"]} }' % self.item_tid + response, status = self.get(self.known_resource, "?where=%s" % where_in) + self.assert200(status) + resource = response["_items"] + self.assertEqual(len(resource), 1) + + self.app.config["DOMAIN"]["contacts"]["query_objectid_as_string"] = True + response, status = self.get(self.known_resource, "?where=%s" % where_in) + self.assert200(status) + resource = response["_items"] + self.assertEqual(len(resource), 0) + def test_get_where_python_syntax(self): where = "ref == %s" % self.item_name response, status = self.get(self.known_resource, "?where=%s" % where) From 26df71f20010b66fa0120cac5fc77e311bf1acc7 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Thu, 16 Jan 2020 16:20:13 +0100 Subject: [PATCH 580/821] The test manufacturer also uses mongo so the skus are mongo objectids. Made a failing test that demonstrates the problem and fixed it. --- eve/io/mongo/mongo.py | 6 +++--- eve/tests/__init__.py | 9 ++++++++- eve/tests/methods/get.py | 7 ++++--- eve/tests/test_settings.py | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index ca8f6351d..f4d1ca626 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -867,13 +867,13 @@ def dict_sub_schema(base): for k, v in source.items(): if isinstance(v, dict): - self._mongotize(v, resource, parse_objectid) # was False + self._mongotize(v, resource, not skip_objectid) elif isinstance(v, list): for i, v1 in enumerate(v): if isinstance(v1, dict): - source[k][i] = self._mongotize(v1, resource, parse_objectid) # was False + source[k][i] = self._mongotize(v1, resource, not skip_objectid) else: - source[k][i] = try_cast(k, v1, parse_objectid) + source[k][i] = try_cast(k, v1, not skip_objectid) elif isinstance(v, str_type): source[k] = try_cast(k, v, parse_objectid) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index bfcb1e9c5..452ddb738 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -433,6 +433,7 @@ def setUp(self, url_converters=None): self.item_tid = contact["tid"] self.item_etag = contact[ETAG] self.item_ref = contact["ref"] + self.item_rows = contact["rows"] self.item_id_url = "/%s/%s" % ( self.domain[self.known_resource]["url"], self.item_id, @@ -519,6 +520,9 @@ def random_contacts(self, num, standard_date_fields=True): contacts.append(contact) return contacts + def to_list_string(self, list_of_strings): + return '["%s"]' % '","'.join(list_of_strings) + def random_users(self, num): users = self.random_contacts(num) for user in users: @@ -569,6 +573,9 @@ def random_products(self, num): def random_string(self, num): return "".join(random.choice(string.ascii_uppercase) for x in range(num)) + def random_hexstring(self, num): + return "".join(random.choice(string.hexdigits).lower() for x in range(num)) + def random_list(self, num): alist = [] for i in range(num): @@ -581,7 +588,7 @@ def random_rows(self, num): for _ in range(num): rows.append( { - "sku": self.random_string(schema["sku"]["maxlength"]), + "sku": self.random_hexstring(schema["sku"]["maxlength"]), "price": random.randint(100, 1000), } ) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 0ce749dfe..31d072211 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -241,17 +241,18 @@ def test_get_where_mongo_objectid_as_string(self): self.assertEqual(len(resource), 0) def test_get_where_mongo_objectid_as_string_but_field_is_id(self): - where_in = '{"tid": { "$in": ["%s"]} }' % self.item_tid + skus = [item["sku"] for item in self.item_rows] + where_in = '{"rows.sku": { "$in": %s} }' % self.to_list_string(skus) response, status = self.get(self.known_resource, "?where=%s" % where_in) self.assert200(status) resource = response["_items"] - self.assertEqual(len(resource), 1) + self.assertEqual(len(resource), 0) self.app.config["DOMAIN"]["contacts"]["query_objectid_as_string"] = True response, status = self.get(self.known_resource, "?where=%s" % where_in) self.assert200(status) resource = response["_items"] - self.assertEqual(len(resource), 0) + self.assertEqual(len(resource), 1) def test_get_where_python_syntax(self): where = "ref == %s" % self.item_name diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 3bffd3438..9dc109175 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -50,7 +50,7 @@ "schema": { "type": "dict", "schema": { - "sku": {"type": "string", "maxlength": 10}, + "sku": {"type": "string", "maxlength": 24}, "price": {"type": "integer"}, }, }, From edc08f19dc348c89f696c8de6a64ef3525111d97 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Thu, 16 Jan 2020 16:29:23 +0100 Subject: [PATCH 581/821] Cleanup _mongotize regarding whether we parse objectid or not. --- eve/io/mongo/mongo.py | 14 ++++++-------- eve/tests/methods/get.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index f4d1ca626..6c8397af1 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -784,7 +784,7 @@ def is_empty(self, resource): ), ) - def _mongotize(self, source, resource, parse_objectid=True): + def _mongotize(self, source, resource): """ Recursively iterates a JSON dictionary, turning RFC-1123 strings into datetime values and ObjectId-link strings into ObjectIds. @@ -807,15 +807,13 @@ def _mongotize(self, source, resource, parse_objectid=True): resource_def = config.DOMAIN[resource] id_field = resource_def["id_field"] id_field_versioned = versioned_id_field(resource_def) - skip_objectid = resource_def.get("query_objectid_as_string", False) + parse_objectid = not resource_def.get("query_objectid_as_string", False) def try_cast(k, v, parse_objectid): try: return datetime.strptime(v, config.DATE_FORMAT) except: - if k in (id_field, id_field_versioned) or ( - parse_objectid and not skip_objectid - ): + if k in (id_field, id_field_versioned) or parse_objectid: try: # Convert to unicode because ObjectId() interprets # 12-character strings (but not unicode) as binary @@ -867,13 +865,13 @@ def dict_sub_schema(base): for k, v in source.items(): if isinstance(v, dict): - self._mongotize(v, resource, not skip_objectid) + self._mongotize(v, resource) elif isinstance(v, list): for i, v1 in enumerate(v): if isinstance(v1, dict): - source[k][i] = self._mongotize(v1, resource, not skip_objectid) + source[k][i] = self._mongotize(v1, resource) else: - source[k][i] = try_cast(k, v1, not skip_objectid) + source[k][i] = try_cast(k, v1, parse_objectid) elif isinstance(v, str_type): source[k] = try_cast(k, v, parse_objectid) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 31d072211..e07d16d19 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -241,8 +241,8 @@ def test_get_where_mongo_objectid_as_string(self): self.assertEqual(len(resource), 0) def test_get_where_mongo_objectid_as_string_but_field_is_id(self): - skus = [item["sku"] for item in self.item_rows] - where_in = '{"rows.sku": { "$in": %s} }' % self.to_list_string(skus) + skus = self.to_list_string([item["sku"] for item in self.item_rows]) + where_in = '{"_id": "%s", "rows.sku": { "$in": %s} }' % (self.item_id, skus) response, status = self.get(self.known_resource, "?where=%s" % where_in) self.assert200(status) resource = response["_items"] From 7dd4dc849feeeed87722f7d9f7739358ff339b41 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Fri, 17 Jan 2020 12:46:50 +0100 Subject: [PATCH 582/821] Fix to use the get_schema_type function we already defined. This allows us to use non id_field fields as ObjectId if they are defined that way in the schema. --- eve/io/mongo/mongo.py | 13 ++++++++----- eve/tests/methods/get.py | 10 ++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 6c8397af1..01bfeb3c9 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -805,15 +805,16 @@ def _mongotize(self, source, resource): .. versionadded:: 0.0.4 """ resource_def = config.DOMAIN[resource] + schema = resource_def.get("schema") id_field = resource_def["id_field"] id_field_versioned = versioned_id_field(resource_def) parse_objectid = not resource_def.get("query_objectid_as_string", False) - def try_cast(k, v, parse_objectid): + def try_cast(k, v, should_parse_objectid): try: return datetime.strptime(v, config.DATE_FORMAT) except: - if k in (id_field, id_field_versioned) or parse_objectid: + if k in (id_field, id_field_versioned) or should_parse_objectid: try: # Convert to unicode because ObjectId() interprets # 12-character strings (but not unicode) as binary @@ -845,7 +846,6 @@ def dict_sub_schema(base): keys = keys[1:] schema_type = schema[k].get("type") if k in schema else None if schema_type == "list": - # TODO: do we need to check for accounts[0] syntax here? if so we need to if "items" in schema[k]: items = schema[k].get("items") or [] possible_types = [get_schema_type(keys, item) for item in items] @@ -864,6 +864,9 @@ def dict_sub_schema(base): return schema_type for k, v in source.items(): + keys = k.split(".") + schema_type = get_schema_type(keys, schema) + is_objectid = (schema_type == "objectid") or parse_objectid if isinstance(v, dict): self._mongotize(v, resource) elif isinstance(v, list): @@ -871,9 +874,9 @@ def dict_sub_schema(base): if isinstance(v1, dict): source[k][i] = self._mongotize(v1, resource) else: - source[k][i] = try_cast(k, v1, parse_objectid) + source[k][i] = try_cast(k, v1, is_objectid) elif isinstance(v, str_type): - source[k] = try_cast(k, v, parse_objectid) + source[k] = try_cast(k, v, is_objectid) return source diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index e07d16d19..eb7948aef 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -238,11 +238,11 @@ def test_get_where_mongo_objectid_as_string(self): response, status = self.get(self.known_resource, "?where=%s" % where) self.assert200(status) resource = response["_items"] - self.assertEqual(len(resource), 0) + self.assertEqual(len(resource), 1) def test_get_where_mongo_objectid_as_string_but_field_is_id(self): skus = self.to_list_string([item["sku"] for item in self.item_rows]) - where_in = '{"_id": "%s", "rows.sku": { "$in": %s} }' % (self.item_id, skus) + where_in = '{"tid": "%s", "rows.sku": { "$in": %s} }' % (self.item_tid, skus) response, status = self.get(self.known_resource, "?where=%s" % where_in) self.assert200(status) resource = response["_items"] @@ -254,6 +254,7 @@ def test_get_where_mongo_objectid_as_string_but_field_is_id(self): resource = response["_items"] self.assertEqual(len(resource), 1) + def test_get_where_python_syntax(self): where = "ref == %s" % self.item_name response, status = self.get(self.known_resource, "?where=%s" % where) @@ -1293,11 +1294,12 @@ def test_get_lookup_field_as_string(self): # of string type and which value is castable to a ObjectId is still # treated as a string when 'query_objectid_as_string' is set to True. # See PR #552. - data = {"id": "507c7f79bcf86cd7994f6c0e", "name": "john"} + self.app.config["DOMAIN"]["contacts"]["query_objectid_as_string"] = True + data = {"id": "507c7f79bcf86cd7994f6c0e", "name": "507c7f79bcf86cd7994f6c0e"} response, status = self.post("ids", data=data) self.assert201(status) - where = '?where={"id": "507c7f79bcf86cd7994f6c0e"}' + where = '?where={"name": "507c7f79bcf86cd7994f6c0e"}' response, status = self.get("ids", where) self.assert200(status) items = response["_items"] From 23c3e526f28400b8d51cff236de5a5c0fac65105 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Fri, 17 Jan 2020 12:51:46 +0100 Subject: [PATCH 583/821] Blacked the get.py test file which was in violation of style rules. --- eve/tests/methods/get.py | 1 - 1 file changed, 1 deletion(-) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index eb7948aef..f4131c27c 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -254,7 +254,6 @@ def test_get_where_mongo_objectid_as_string_but_field_is_id(self): resource = response["_items"] self.assertEqual(len(resource), 1) - def test_get_where_python_syntax(self): where = "ref == %s" % self.item_name response, status = self.get(self.known_resource, "?where=%s" % where) From a065c6f1e068336f6cf4c9eca3ed38ca7081fd1b Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Mon, 20 Jan 2020 14:14:34 +0100 Subject: [PATCH 584/821] Update test to make sure every contact has rows. --- eve/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 452ddb738..33019ba48 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -499,7 +499,7 @@ def random_contacts(self, num, standard_date_fields=True): "prog": i, "role": random.choice(schema["role"]["allowed"]), "title": schema["title"]["default"], - "rows": self.random_rows(random.randint(0, 5)), + "rows": self.random_rows(random.randint(1, 5)), "alist": self.random_list(random.randint(0, 5)), "location": { "address": "address " + self.random_string(5), From 534371ace5cbc42216c877b1a39c6ff2a428e890 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Tue, 21 Jan 2020 13:54:32 +0100 Subject: [PATCH 585/821] Add a failing test for where?{"tid": { "$in": ["..."]}} which is a nested document and needs the fact that tid is an object id so the state should be pushed down into the recursion. --- eve/io/mongo/mongo.py | 7 ++++--- eve/tests/methods/get.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 01bfeb3c9..001a17b69 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -784,7 +784,7 @@ def is_empty(self, resource): ), ) - def _mongotize(self, source, resource): + def _mongotize(self, source, resource, parse_objectid=False): """ Recursively iterates a JSON dictionary, turning RFC-1123 strings into datetime values and ObjectId-link strings into ObjectIds. @@ -808,7 +808,8 @@ def _mongotize(self, source, resource): schema = resource_def.get("schema") id_field = resource_def["id_field"] id_field_versioned = versioned_id_field(resource_def) - parse_objectid = not resource_def.get("query_objectid_as_string", False) + query_objectid_as_string = resource_def.get("query_objectid_as_string", False) + parse_objectid = parse_objectid or not query_objectid_as_string def try_cast(k, v, should_parse_objectid): try: @@ -868,7 +869,7 @@ def dict_sub_schema(base): schema_type = get_schema_type(keys, schema) is_objectid = (schema_type == "objectid") or parse_objectid if isinstance(v, dict): - self._mongotize(v, resource) + self._mongotize(v, resource, is_objectid) elif isinstance(v, list): for i, v1 in enumerate(v): if isinstance(v1, dict): diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index f4131c27c..2eee5f83a 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -240,6 +240,19 @@ def test_get_where_mongo_objectid_as_string(self): resource = response["_items"] self.assertEqual(len(resource), 1) + def test_get_where_mongo_objectid_as_string_with_nested_documents(self): + where = '{"tid": { "$in": ["%s"]}}' % self.item_tid + response, status = self.get(self.known_resource, "?where=%s" % where) + self.assert200(status) + resource = response["_items"] + self.assertEqual(len(resource), 1) + + self.app.config["DOMAIN"]["contacts"]["query_objectid_as_string"] = True + response, status = self.get(self.known_resource, "?where=%s" % where) + self.assert200(status) + resource = response["_items"] + self.assertEqual(len(resource), 1) + def test_get_where_mongo_objectid_as_string_but_field_is_id(self): skus = self.to_list_string([item["sku"] for item in self.item_rows]) where_in = '{"tid": "%s", "rows.sku": { "$in": %s} }' % (self.item_tid, skus) From b2fcae43a71ee692ca29b3e939fe1d02276c7bd6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 26 Jan 2020 09:44:42 +0100 Subject: [PATCH 586/821] Changelog for #1347 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index de2e02932..21f026cd1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones +- Fix: Mixing foreign and local object ids breaks querying (`#1345`_) + +.. _`#1345`: https://github.com/pyeve/eve/issues/1345 Version 1.0 ----------- From 8fcf678e95b70ae8263cf204e21b7408908a6f29 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 26 Jan 2020 09:46:45 +0100 Subject: [PATCH 587/821] Ewan Higgs --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index e2afce16c..8825b3cf4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` + - Aayush Sarva - Adam Walsh - Alberto Marin @@ -53,6 +54,7 @@ Patches and Contributions - Einar Huseby - Emmanuel Leblond - Eugene Prikazchikov +- Ewan Higgs - Felix Peppert - Florian Rathgeber - Francisco Corrales Morales From 52a927fe69ff05d3098d62145efe1fbfaddb5cf9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 26 Jan 2020 09:58:43 +0100 Subject: [PATCH 588/821] Bump version to 1.0.1 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 21f026cd1..b56b93272 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 1.0.1 +------------- + +Released on January 26, 2020. + - Fix: Mixing foreign and local object ids breaks querying (`#1345`_) .. _`#1345`: https://github.com/pyeve/eve/issues/1345 diff --git a/eve/__init__.py b/eve/__init__.py index 4d97d8faa..ebcbb3ee3 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.0" +__version__ = "1.0.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 2ca1d8e807c16a53d583aab209276ceb6a45a3c8 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 26 Jan 2020 22:29:01 +1100 Subject: [PATCH 589/821] Fix simple typo: wether -> whether Closes #1348 --- eve/io/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/base.py b/eve/io/base.py index 82bf793b4..41e893345 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -134,7 +134,7 @@ def find(self, resource, req, sub_resource_lookup, perform_count=True): to support with your driver. For example ``eve.io.Mongo`` supports both Python and Mongo-like query syntaxes. :param sub_resource_lookup: sub-resource lookup from the endpoint url. - :param perform_count: wether a document count should be performed and + :param perform_count: whether a document count should be performed and returned to the client. .. versionchanged:: 0.3 From e99b0e3fd3a53e2a2f9719dfdf0c9a4a91f37966 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:21:35 +0100 Subject: [PATCH 590/821] Changelog for #1349 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b56b93272..b7445d283 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Documentation typos (`#1348`_) + +.. _`#1348`: https://github.com/pyeve/eve/pull/1348 Version 1.0.1 ------------- From bcf314a35952cd49fe872421ba91bb6d824a68bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:21:42 +0100 Subject: [PATCH 591/821] Tim Gates --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 8825b3cf4..9d818f1c3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,7 +9,6 @@ Development Lead Patches and Contributions ````````````````````````` - - Aayush Sarva - Adam Walsh - Alberto Marin @@ -178,6 +177,7 @@ Patches and Contributions - Tano Abeleyra - Taylor Brown - Thomas Sileo +- Tim Gates - Tim Jacobi - Tomasz Jezierski - Valerie Coffman From b497e18e73ac8af9716f85e6117bc9a240b6aa3c Mon Sep 17 00:00:00 2001 From: Tyler Kennedy Date: Wed, 29 Jan 2020 17:01:17 -0500 Subject: [PATCH 592/821] Fix link to extensions on documentation homepage. RTD needs a suffix, `extensions` -> `extensions.html`. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 9a404b18b..9e71ad8b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -127,4 +127,4 @@ is also a simple `client app`_ available. .. _Redis: http://redis.io .. _Cerberus: http://python-cerberus.org .. _events: https://github.com/pyeve/events -.. _extensions: http://python-eve.org/extensions +.. _extensions: http://python-eve.org/extensions.html From 7f3c76f4b07fcdb65c065498c47029777eb0d80f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:25:21 +0100 Subject: [PATCH 593/821] Changelog for #1350 --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b7445d283..c4d1ef7a4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,10 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Documentation typos (`#1348`_) +- Documentation typos (`#1348`_, `#1350`_) -.. _`#1348`: https://github.com/pyeve/eve/pull/1348 +.. _`#1350`: https://github.com/pyeve/eve/pull/1350 +.. _`#1348`: https://github.com/pyeve/eve/issues/1348 Version 1.0.1 ------------- From 6b50f476185f6f5ea1e65ecdae08c5f9577acdd9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:25:30 +0100 Subject: [PATCH 594/821] Tyler Kennedy --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 9d818f1c3..3688d6896 100644 --- a/AUTHORS +++ b/AUTHORS @@ -180,6 +180,7 @@ Patches and Contributions - Tim Gates - Tim Jacobi - Tomasz Jezierski +- Tyler Kennedy - Valerie Coffman - Vasilis Lolis - Vincent Bisserie From ce32db3f45f3932a8fdaf4fb41efbb4612b4f0ee Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Thu, 30 Jan 2020 20:43:10 +0100 Subject: [PATCH 595/821] Add MONGO_QUERY_WHITELIST global and resource config options Added also $eq as default supported operator --- docs/config.rst | 13 +++++++++++++ eve/default_settings.py | 4 ++++ eve/flaskapp.py | 6 ++++++ eve/io/mongo/mongo.py | 21 +++++++++++++-------- eve/tests/config.py | 4 ++++ eve/tests/methods/get.py | 22 ++++++++++++++++++++++ 6 files changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 90d17ce9d..a48c265a2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -580,6 +580,15 @@ uppercase. easily replaced with the (very rich) Mongo query dialect. +``MONGO_QUERY_WHITELIST`` A list of extra Mongo query operators to allow + besides the official list of allowed operators. + Defaults to ``[]``. + + Can be overridden at endpoint (Mongo + collection) level. See + ``mongo_query_whitelist`` below. + + ``MONGO_WRITE_CONCERN`` A dictionary defining MongoDB write concern settings. All standard write concern settings (w, wtimeout, j, fsync) are @@ -1005,6 +1014,10 @@ always lowercase. :ref:`hateoas_feature` for the resource. Defaults to ``True``. +``mongo_query_whitelist`` A list of extra Mongo query operators to allow + for this endpoint besides the official list of + allowed operators. Defaults to ``[]``. + ``mongo_write_concern`` A dictionary defining MongoDB write concern settings for the endpoint datasource. All standard write concern settings (w, wtimeout, j, diff --git a/eve/default_settings.py b/eve/default_settings.py index 5d9e98259..cc45e9056 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -11,6 +11,9 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. + .. versionchanged:: 1.1.0 + 'MONGO_QUERY_WHITELIST' added and set to emtpy list. + .. versionchanged:: 0.8 'RENDERERS' added with XML and JSON renderers. 'JSON' removed. @@ -261,6 +264,7 @@ # attacks ('ReDoS' especially), are probably too complex for the average API # end-user and finally can seriously impact overall performance. MONGO_QUERY_BLACKLIST = ["$where", "$regex"] +MONGO_QUERY_WHITELIST = [] # Explicitly set default write_concern to 'safe' (do regular # aknowledged writes). This is also the current PyMongo/Mongo default setting. MONGO_WRITE_CONCERN = {"w": 1} diff --git a/eve/flaskapp.py b/eve/flaskapp.py index cf07ccd0e..85be744a1 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -598,6 +598,9 @@ def set_defaults(self): def _set_resource_defaults(self, resource, settings): """ Low-level method which sets default values for one resource. + .. versionchanged:: 1.1.0 + Added 'mongo_query_whitelist'. + .. versionchanged:: 0.6.2 Fix: startup crash when both SOFT_DELETE and ALLOW_UNKNOWN are True. @@ -668,6 +671,9 @@ def _set_resource_defaults(self, resource, settings): settings.setdefault( "extra_response_fields", self.config["EXTRA_RESPONSE_FIELDS"] ) + settings.setdefault( + "mongo_query_whitelist", self.config["MONGO_QUERY_WHITELIST"] + ) settings.setdefault("mongo_write_concern", self.config["MONGO_WRITE_CONCERN"]) settings.setdefault("mongo_indexes", {}) settings.setdefault("hateoas", self.config["HATEOAS"]) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 001a17b69..159d15a6c 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -117,7 +117,7 @@ class Mongo(DataLayer): json_encoder_class = MongoJSONEncoder operators = set( - ["$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin"] + ["$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$eq"] + ["$or", "$and", "$not", "$nor"] + ["$mod", "$regex", "$text", "$where"] + ["$options", "$search", "$language", "$caseSensitive"] @@ -220,7 +220,7 @@ def find(self, resource, req, sub_resource_lookup, perform_count=True): # return an error) client_sort = self._convert_sort_request_to_dict(req) - spec = self._convert_where_request_to_dict(req) + spec = self._convert_where_request_to_dict(resource, req) bad_filter = validate_filters(spec, resource) if bad_filter: @@ -881,10 +881,14 @@ def dict_sub_schema(base): return source - def _sanitize(self, spec): + def _sanitize(self, resource, spec): """ Makes sure that only allowed operators are included in the query, aborts with a 400 otherwise. + .. versionchanged:: 1.1.0 + Add mongo_query_whitelist config option to extend the list of + supported operators + .. versionchanged:: 0.5 Abort with 400 if unsupported query operators are used. #387. DRY. @@ -898,7 +902,8 @@ def _sanitize(self, spec): def sanitize_keys(spec): ops = set([op for op in spec.keys() if op[0] == "$"]) - unknown = ops - Mongo.operators + known = Mongo.operators | set(config.DOMAIN[resource]["mongo_query_whitelist"]) + unknown = ops - known if unknown: abort( 400, @@ -919,10 +924,10 @@ def sanitize_keys(spec): if isinstance(spec, dict): sanitize_keys(spec) for value in spec.values(): - self._sanitize(value) + self._sanitize(resource, value) if isinstance(spec, list): for value in spec: - self._sanitize(value) + self._sanitize(resource, value) return spec @@ -951,14 +956,14 @@ def _convert_sort_request_to_dict(self, req): abort(400, description=debug_error_message(str(e))) return client_sort - def _convert_where_request_to_dict(self, req): + def _convert_where_request_to_dict(self, resource, req): """ Converts the contents of a `ParsedRequest`'s `where` property to a dict """ query = {} if req and req.where: try: - query = self._sanitize(json.loads(req.where)) + query = self._sanitize(resource, json.loads(req.where)) except HTTPException: # _sanitize() is raising an HTTP exception; let it fire. raise diff --git a/eve/tests/config.py b/eve/tests/config.py index 85d4e6f04..8f2e53b9e 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -57,6 +57,7 @@ def test_default_settings(self): self.assertEqual(self.app.config["MONGO_HOST"], "localhost") self.assertEqual(self.app.config["MONGO_PORT"], 27017) self.assertEqual(self.app.config["MONGO_QUERY_BLACKLIST"], ["$where", "$regex"]) + self.assertEqual(self.app.config["MONGO_QUERY_WHITELIST"], []) self.assertEqual(self.app.config["MONGO_WRITE_CONCERN"], {"w": 1}) self.assertEqual(self.app.config["ISSUES"], "_issues") @@ -268,6 +269,9 @@ def _test_defaults_for_resource(self, resource): self.assertEqual( settings["extra_response_fields"], self.app.config["EXTRA_RESPONSE_FIELDS"] ) + self.assertEqual( + settings["mongo_query_whitelist"], self.app.config["MONGO_QUERY_WHITELIST"] + ) self.assertEqual( settings["mongo_write_concern"], self.app.config["MONGO_WRITE_CONCERN"] ) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2eee5f83a..f0342b63e 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -227,6 +227,28 @@ def test_get_mongo_query_blacklist_nested(self): _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) + def test_get_mongo_query_whitelist(self): + where = '{"$expr": {"$eq": [{"$year": "$_created"}, 2020]}}' + _, status = self.get(self.known_resource, "?where=%s" % where) + self.assert400(status) + + settings = self.app.config["DOMAIN"][self.known_resource] + settings["mongo_query_whitelist"] = ["$year"] + _, status = self.get(self.known_resource, "?where=%s" % where) + self.assert200(status) + + def test_get_mongo_query_whitelist_nested(self): + where = ( + '{"$or": [{"$expr": {"$eq": [{"$year": "$_created"}, 2020]}}]}' + ) + _, status = self.get(self.known_resource, "?where=%s" % where) + self.assert400(status) + + settings = self.app.config["DOMAIN"][self.known_resource] + settings["mongo_query_whitelist"] = ["$year"] + _, status = self.get(self.known_resource, "?where=%s" % where) + self.assert200(status) + def test_get_where_mongo_objectid_as_string(self): where = '{"tid": "%s"}' % self.item_tid response, status = self.get(self.known_resource, "?where=%s" % where) From 819d102ce9bb110a2a88a2a53b8752a6a066b254 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Thu, 30 Jan 2020 23:16:04 +0100 Subject: [PATCH 596/821] Corrections for black --- eve/io/mongo/mongo.py | 5 ++++- eve/tests/methods/get.py | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 159d15a6c..d91d7f2b7 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -902,7 +902,10 @@ def _sanitize(self, resource, spec): def sanitize_keys(spec): ops = set([op for op in spec.keys() if op[0] == "$"]) - known = Mongo.operators | set(config.DOMAIN[resource]["mongo_query_whitelist"]) + known = Mongo.operators | set( + config.DOMAIN[resource]["mongo_query_whitelist"] + ) + unknown = ops - known if unknown: abort( diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index f0342b63e..d1353d7fd 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -238,9 +238,7 @@ def test_get_mongo_query_whitelist(self): self.assert200(status) def test_get_mongo_query_whitelist_nested(self): - where = ( - '{"$or": [{"$expr": {"$eq": [{"$year": "$_created"}, 2020]}}]}' - ) + where = '{"$or": [{"$expr": {"$eq": [{"$year": "$_created"}, 2020]}}]}' _, status = self.get(self.known_resource, "?where=%s" % where) self.assert400(status) From ebe5915cc91b2746cfdd18710c462f03497be58c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:39:00 +0100 Subject: [PATCH 597/821] Changelog for #1352 --- CHANGES.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c4d1ef7a4..18ee96748 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,18 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +New +~~~ +- ``MONGO_QUERY_WHITELIST`` and ``mongo_query_whitelist``. A list of extra Mongo + query operators to allow besides the official list of allowed operators. + Defaults to ``[]``. (`#1351`_) + +Fixed +~~~~~ +- ``$eq`` is missing from supported query opeators (`#1351`_) - Documentation typos (`#1348`_, `#1350`_) +.. _`#1351`: https://github.com/pyeve/eve/issues/1351 .. _`#1350`: https://github.com/pyeve/eve/pull/1350 .. _`#1348`: https://github.com/pyeve/eve/issues/1348 From 4d6613aad8cd713c376a353dbb866fcbd070f2bc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 1 Feb 2020 09:44:38 +0100 Subject: [PATCH 598/821] Bump version to 1.1.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index ebcbb3ee3..ee323fb1b 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.0.1" +__version__ = "1.1.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 98120451de80216d55bd5642a8fc9d669ac9a8a0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Feb 2020 14:45:16 +0100 Subject: [PATCH 599/821] Fix: Werkzeug 1.0 crash Closes #1359 --- CHANGES.rst | 2 ++ eve/methods/get.py | 2 +- eve/tests/methods/get.py | 3 +-- eve/utils.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 18ee96748..8167967ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,9 +14,11 @@ New Fixed ~~~~~ +- Starup crash with Werkzeug 1.0 (`#1359`_) - ``$eq`` is missing from supported query opeators (`#1351`_) - Documentation typos (`#1348`_, `#1350`_) +.. _`#1359`: https://github.com/pyeve/eve/issues/1359 .. _`#1351`: https://github.com/pyeve/eve/issues/1351 .. _`#1350`: https://github.com/pyeve/eve/pull/1350 .. _`#1348`: https://github.com/pyeve/eve/issues/1348 diff --git a/eve/methods/get.py b/eve/methods/get.py index 82ab48896..9f08be17a 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -15,7 +15,7 @@ import copy import simplejson as json from flask import current_app as app, abort, request -from werkzeug import MultiDict +from werkzeug.datastructures import MultiDict from .common import ( ratelimit, diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index d1353d7fd..45640c995 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -6,12 +6,11 @@ from bson import ObjectId from bson.dbref import DBRef from bson.son import SON -from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.datastructures import ImmutableMultiDict, MultiDict from eve.tests import TestBase from eve.tests.utils import DummyEvent from eve.tests.test_settings import MONGO_DBNAME from eve.utils import str_to_date, date_to_rfc1123 -from werkzeug import MultiDict from eve.methods.get import get_internal, getitem_internal diff --git a/eve/utils.py b/eve/utils.py index cd8acde31..4498edc9f 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -22,7 +22,7 @@ from datetime import datetime, timedelta from bson.json_util import dumps from eve import RFC1123_DATE_FORMAT -from werkzeug import MultiDict +from werkzeug.datastructures import MultiDict class Config(object): From 610396d28e6b0a2127b597feff6922980feb264f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 7 Feb 2020 15:11:29 +0100 Subject: [PATCH 600/821] Bump version to 1.1 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8167967ab..6dcb9f1e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 1.1 +----------- + +Released on February 7, 2020. + New ~~~ - ``MONGO_QUERY_WHITELIST`` and ``mongo_query_whitelist``. A list of extra Mongo diff --git a/eve/__init__.py b/eve/__init__.py index ee323fb1b..d4e47fdbd 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.dev0" +__version__ = "1.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 69fc4668f21164eac470c7cf981c178e68cdb347 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 29 Feb 2020 14:45:14 +0100 Subject: [PATCH 601/821] Bump version to 1.1.1.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index d4e47fdbd..192108eca 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1" +__version__ = "1.1.1.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From e4f5a4194fda139aa637ab8181a3183e2a26646e Mon Sep 17 00:00:00 2001 From: elias-garcia Date: Wed, 26 Feb 2020 10:49:24 +0100 Subject: [PATCH 602/821] Fix unique constraint in nested attributes Closes #1360 --- eve/io/mongo/validation.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index a532abce8..59f06b381 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -92,7 +92,18 @@ def _is_value_unique(self, unique, field, value, query): .. versionadded:: 0.6 """ if unique: - query[field] = value + schema = self.schema + attribute_path = self.document_path + (field,) + temp_path = [attribute_path[0]] + + for i, path in enumerate(attribute_path[:-1]): + schema = schema[path] + if schema["type"] != "list": + temp_path.append(attribute_path[i + 1]) + + final_path = ".".join(temp_path) + + query[final_path] = value resource_config = config.DOMAIN[self.resource] From a4621958e0175371b378f3abfcc73c88048f7f5a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 29 Feb 2020 14:35:57 +0100 Subject: [PATCH 603/821] Changelog for #1361 --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6dcb9f1e9..3a00f4ccf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +Fixed +~~~~~ + +- ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) + +.. _`#1360`: https://github.com/pyeve/eve/issues/1360 Version 1.1 ----------- From 7077c3cf1666062bbbcfefaaf6aea3c089e8f29b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 29 Feb 2020 14:36:59 +0100 Subject: [PATCH 604/821] =?UTF-8?q?Elias=20Garc=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 3688d6896..7bf5eb994 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Development Lead Patches and Contributions ````````````````````````` + - Aayush Sarva - Adam Walsh - Alberto Marin @@ -51,6 +52,7 @@ Patches and Contributions - Dong Wei Ming - Dougal Matthews - Einar Huseby +- Elias García - Emmanuel Leblond - Eugene Prikazchikov - Ewan Higgs From 39168f50b201f3ead4c04b05002e60e6710dfa6e Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 1 Mar 2020 17:50:17 +0000 Subject: [PATCH 605/821] Add 403 Forbidden to STANDARD_ERRORS This isn't actually an error the Eve Framework will ever raise. As far as I could tell. However, separation between authentication and authorization is a must even for less complex applicatons. 403 is the most adequate response a server can provide to client whose authentication was accepted, yet for some reason or another the server will not comply with the request. The Eve Framework documentation suggests the usage of 403 in, at least two instances: * `abort(403)` in: https://docs.python-eve.org/en/stable/features.html#database-event-hooks * The actual documentation for the `STANDARD_ERRORS` config variable, describes 403 as a supported code, in: https://docs.python-eve.org/en/stable/config.html#global-configuration --- eve/default_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/default_settings.py b/eve/default_settings.py index cc45e9056..9c3b38250 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -131,7 +131,7 @@ # codes for which we want to return a standard response which includes # a JSON body with the status, code, and description. -STANDARD_ERRORS = [400, 401, 404, 405, 406, 409, 410, 412, 422, 428, 429] +STANDARD_ERRORS = [400, 401, 403, 404, 405, 406, 409, 410, 412, 422, 428, 429] # field returned on GET requests so we know if we have the latest copy even if # we access a specific version From ab21f0c2d81d668e0a4109aaf68538250e1a065a Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 1 Mar 2020 19:38:27 +0000 Subject: [PATCH 606/821] fix config.test_default_settings --- eve/tests/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/tests/config.py b/eve/tests/config.py index 8f2e53b9e..08d7f67f9 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -85,7 +85,7 @@ def test_default_settings(self): self.assertEqual(self.app.config["SHOW_DELETED_PARAM"], "show_deleted") self.assertEqual( self.app.config["STANDARD_ERRORS"], - [400, 401, 404, 405, 406, 409, 410, 412, 422, 428, 429], + [400, 401, 403, 404, 405, 406, 409, 410, 412, 422, 428, 429], ) self.assertEqual(self.app.config["UPSERT_ON_PUT"], True) self.assertEqual( From d8074e4c3624353c6259e8ffdf4068db42eeebad Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 14 Mar 2020 08:17:46 +0100 Subject: [PATCH 607/821] Changelog for #1362 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3a00f4ccf..658cbe3a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,8 +9,10 @@ In Development Fixed ~~~~~ +- 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) +.. _`#1362`: https://github.com/pyeve/eve/pull/1362 .. _`#1360`: https://github.com/pyeve/eve/issues/1360 Version 1.1 From c0cfd35d57b14d861957a1a240630bcd0351d3bf Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Tue, 10 Mar 2020 11:51:25 +0100 Subject: [PATCH 608/821] Fix support for dicts without schema rule --- eve/io/mongo/mongo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index d91d7f2b7..8d9fce156 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -857,10 +857,11 @@ def dict_sub_schema(base): elif "schema" in schema[k]: # recursively check the schema return get_schema_type( - keys, dict_sub_schema(schema[k].get("schema")) + keys, dict_sub_schema(schema[k]["schema"]) ) elif schema_type == "dict": - return get_schema_type(keys, dict_sub_schema(schema[k].get("schema"))) + if "schema" in schema[k]: + return get_schema_type(keys, dict_sub_schema(schema[k]["schema"])) else: return schema_type From 0117087cc8564a38ddac1f625c07f8ae7e3a109a Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Tue, 10 Mar 2020 12:14:10 +0100 Subject: [PATCH 609/821] Change formatting to make black happy --- eve/io/mongo/mongo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 8d9fce156..b84f4bc9b 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -856,9 +856,7 @@ def dict_sub_schema(base): return next((t for t in possible_types if t), None) elif "schema" in schema[k]: # recursively check the schema - return get_schema_type( - keys, dict_sub_schema(schema[k]["schema"]) - ) + return get_schema_type(keys, dict_sub_schema(schema[k]["schema"])) elif schema_type == "dict": if "schema" in schema[k]: return get_schema_type(keys, dict_sub_schema(schema[k]["schema"])) From 57e9d02479a1a9d7c0fa881009d0cd756368d0fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 14 Mar 2020 08:22:21 +0100 Subject: [PATCH 610/821] Changelog for #1366 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 658cbe3a0..870c435fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,9 +9,11 @@ In Development Fixed ~~~~~ +- dics without ``schema`` rule are broken since v1.1 (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) +.. _`#1366`: https://github.com/pyeve/eve/pull/1366 .. _`#1362`: https://github.com/pyeve/eve/pull/1362 .. _`#1360`: https://github.com/pyeve/eve/issues/1360 From a486c21d5752525b1fec2cea89c4c83b29108dcd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 14 Mar 2020 08:39:43 +0100 Subject: [PATCH 611/821] Changelog correction --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 870c435fb..91ef0e4d2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,7 @@ In Development Fixed ~~~~~ -- dics without ``schema`` rule are broken since v1.1 (`#1366`_) +- dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) From c09cbfa76040e5c1ee668ab47ab2ce95369503ac Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Sat, 14 Mar 2020 16:50:49 +0100 Subject: [PATCH 612/821] Fix unique_within_resource rule used in resources without datasource filter --- docs/config.rst | 4 +++- eve/io/mongo/validation.py | 2 ++ eve/tests/methods/post.py | 16 +++++++++++++++- eve/tests/test_settings.py | 12 ++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index a48c265a2..6f74cba8c 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1315,7 +1315,9 @@ defining the field validation rules. Allowed validation rules are: Use this when the resource shares the database collection with other resources but their documents should not be taken into account when evaluating - the uniqueness of the field. + the uniqueness of the field. When used in a resource + without datasource filter, this rule behaves like + ``unique``. ``data_relation`` Allows to specify a referential integrity rule that the value must satisfy in order to diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 59f06b381..7abe7bf34 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -77,6 +77,8 @@ def _validate_unique_to_user(self, unique, field, value): def _validate_unique_within_resource(self, unique, field, value): """ {'type': 'boolean'} """ _, filter_, _, _ = app.data.datasource(self.resource) + if filter_ is None: + filter_ = {} self._is_value_unique(unique, field, value, filter_) def _validate_unique(self, unique, field, value): diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 53843ecdb..5ead462ba 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -975,12 +975,26 @@ def test_post_projection_is_honored(self): self.assertTrue("ref" in r) self.assertTrue("aninteger" not in r) - def test_unique_value_different_resources(self): + def test_unique_within_resource_value_different_resources(self): r, status = self.post("tenant_a", data={"name": "John"}) self.assert201(status) r, status = self.post("tenant_b", data={"name": "John"}) self.assert201(status) + def test_unique_within_resource_in_resource_without_filter(self): + r, status = self.post( + "test_unique", data={"unique_within_resource_attribute": "unique_value"} + ) + self.assert201(status) + r, status = self.post( + "test_unique", data={"unique_within_resource_attribute": "unique_value"} + ) + self.assert422(status) + r, status = self.post( + "test_unique", data={"unique_within_resource_attribute": "unique_value 2"} + ) + self.assert201(status) + def perform_post(self, data, valid_items=[0]): r, status = self.post(self.known_resource_url, data=data) self.assert201(status) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 9dc109175..d216bbae7 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -298,6 +298,17 @@ }, } +test_unique = { + "datasource": {"source": "test_unique"}, + "schema": { + "unique_attribute": {"type": "string", "unique": True}, + "unique_within_resource_attribute": { + "type": "string", + "unique_within_resource": True, + }, + }, +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -334,4 +345,5 @@ "test_patch": test_patch, "tenant_a": tenant_a, "tenant_b": tenant_b, + "test_unique": test_unique, } From c9ea3b44b7b0240d01d41d05bbf33a840b3c75b1 Mon Sep 17 00:00:00 2001 From: Arnau Orriols Date: Mon, 16 Mar 2020 14:36:32 +0100 Subject: [PATCH 613/821] Remove trailing whitespace --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 6f74cba8c..d58a34bab 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1317,7 +1317,7 @@ defining the field validation rules. Allowed validation rules are: should not be taken into account when evaluating the uniqueness of the field. When used in a resource without datasource filter, this rule behaves like - ``unique``. + ``unique``. ``data_relation`` Allows to specify a referential integrity rule that the value must satisfy in order to From 99e0c99a1acf5cd891a89f2c42c10b67d11ca799 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Apr 2020 09:31:10 +0200 Subject: [PATCH 614/821] Changelog for #1368 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 91ef0e4d2..d7f661ef5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,10 +9,12 @@ In Development Fixed ~~~~~ +- Fix ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) - dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) +.. _`#1368`: https://github.com/pyeve/eve/pull/1368 .. _`#1366`: https://github.com/pyeve/eve/pull/1366 .. _`#1362`: https://github.com/pyeve/eve/pull/1362 .. _`#1360`: https://github.com/pyeve/eve/issues/1360 From 061dc833046c2702d0c8bd039fb499df2af4f8d4 Mon Sep 17 00:00:00 2001 From: Henry Longmore Date: Thu, 26 Mar 2020 16:34:16 -0600 Subject: [PATCH 615/821] Make sure unit tests pass first (minor refactor, update docs) py27 and py36 commands succeeded. --- CONTRIBUTING.rst | 6 +++++- eve/tests/endpoints.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a8a5d7c2e..a9a901bbd 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -137,7 +137,11 @@ installed to run all of the environments. Then run:: tox Please note that you need an active MongoDB instance running on localhost in -order for the tests run. Also, be advised that in order to execute the +order for the tests run. Save yourself some time and headache by creating a +MongoDB user with the password defined in the `test_settings.py` file in the +admin database (the pre-commit process is unforgiving if you don't want to +commit your admin credentials but still have the file modified, which would be +necessary for tox). Also, be advised that in order to execute the :ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting tests are silently skipped if any of the two conditions are not met. diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index bdbaf5e0f..ec17598e6 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -7,7 +7,11 @@ from datetime import datetime from eve.utils import config from eve.io.base import BaseJSONEncoder -from eve.tests.test_settings import MONGO_DBNAME +from eve.tests.test_settings import ( + MONGO_DBNAME, + MONGO_USERNAME, + MONGO_PASSWORD, +) from uuid import UUID from eve.io.mongo import Validator import os @@ -68,9 +72,9 @@ def setUp(self): "schema": {"_id": {"type": "uuid"}, "name": {"type": "string"}}, } settings = { - "MONGO_USERNAME": "test_user", - "MONGO_PASSWORD": "test_pw", - "MONGO_DBNAME": "eve_test", + "MONGO_USERNAME": MONGO_USERNAME, + "MONGO_PASSWORD": MONGO_PASSWORD, + "MONGO_DBNAME": MONGO_DBNAME, "DOMAIN": {"uuids": uuids}, } url_converters = {"uuid": UUIDConverter} From b7c1433c003a9c41fd523c0fa2d399eb8708bee2 Mon Sep 17 00:00:00 2001 From: Henry Longmore Date: Fri, 27 Mar 2020 23:36:14 -0600 Subject: [PATCH 616/821] Add resource and commented out test so tox passes --- eve/tests/methods/post.py | 40 ++++++++++++++++++++++++++++++++++++++ eve/tests/test_settings.py | 21 ++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 5ead462ba..bccec352c 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -657,6 +657,46 @@ def test_post_bandwidth_saver(self): ) self.assertEqual(etag, r[self.app.config["ETAG"]]) + # def test_post_bandwidth_saver_credit_rule_broken(self): + # data = [ + # { + # "amount": 300.0, + # "duration": "months", + # "name": "Bandwidth Saver:True, Projection:True", + # "start": "2020-03-28T06:00:00 UTC" + # } + # ] + # + # # bandwidth_saver is on by default + # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + # self.assertTrue(self.app.config["PROJECTION"]) + # r, status = self.post("credit_rules", data=data) + # self.assert201(status) + # self.assertPostResponse(r) + # self.assertFalse("amount" in r) + # etag = r[self.app.config["ETAG"]] + # r, status = self.get( + # "credit_rules", "", + # r[self.domain["credit_rules"]["id_field"]] + # ) + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + # + # # test return all fields (bandwidth_saver off) + # self.app.config["BANDWIDTH_SAVER"] = False + # r, status = self.post("credit_rules", data=data) + # self.assert201(status) + # self.assertPostResponse(r) + # self.assertTrue( + # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + # 'One or more of "amount", "duration", "name", "start" is missing.' + # ) + # etag = r[self.app.config["ETAG"]] + # r, status = self.get( + # "credit_rules", "", + # r[self.domain["credit_rules"]["id_field"]] + # ) + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_post_alternative_payload(self): payl = {"ref": "5432112345678901234567890", "role": ["agent"]} with self.app.test_request_context(self.known_resource_url): diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index d216bbae7..3f07fedec 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -309,6 +309,26 @@ }, } +credit_rules = { + "allow_unknown": True, + "schema": { + "name": {"type": "string"}, + "amount": {"type": "float", "default": 0.00, "min": 0.00, "required": True}, + "start": {"type": "string", "required": True}, + "duration": { + "type": "string", + "allowed": ["days", "weeks", "months", "years", "one-time"], + "required": True, + }, + "prepaid": {"type": "boolean", "default": False}, + "expiration_duration": { + "type": "string", + "allowed": ["days", "weeks", "months", "years"], + "required": False, + } + } +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -346,4 +366,5 @@ "tenant_a": tenant_a, "tenant_b": tenant_b, "test_unique": test_unique, + "credit_rules": credit_rules, } From 6cb43127d1a0cb7d5f5d60772cd8a0ef04b9381c Mon Sep 17 00:00:00 2001 From: Henry Longmore Date: Sat, 28 Mar 2020 00:16:54 -0600 Subject: [PATCH 617/821] Add commented out put test so tox passes --- eve/tests/methods/put.py | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index ddb0d6a58..2358181a1 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -362,6 +362,52 @@ def test_put_bandwidth_saver(self): db_value = self.compare_put_with_get(self.app.config["ETAG"], r) self.assertEqual(db_value, r[self.app.config["ETAG"]]) + # def test_put_bandwidth_saver_credit_rule_broken(self): + # _db = self.connection[MONGO_DBNAME] + # rule = { + # "amount": 300.0, + # "duration": "months", + # "name": "Testing BANDWIDTH_SAVER=False", + # "start": "2020-03-28T06:00:00 UTC", + # } + # rule_id = _db.credit_rules.insert_one(rule).inserted_id + # rule_url = "credit_rules/%s/" % (rule_id) + # changes = { + # "amount": 120.0, + # "duration": "months", + # "start": "2020-04-01T00:00:00 UTC", + # } + # response, _ = self.get("credit_rules/%s/" % (rule_id)) + # etag = response[ETAG] + # # bandwidth_saver is on by default + # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + # self.assertTrue(self.app.config["PROJECTION"]) + # r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) + # self.assert200(status) + # self.assertPutResponse(r, "%s" % (rule_id)) + # self.assertFalse("amount" in r) + # etag = r[self.app.config["ETAG"]] + # r, _ = self.get(rule_url, "") + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + # + # # test return all fields (bandwidth_saver off) + # self.app.config["BANDWIDTH_SAVER"] = False + # changes["name"] = "Give it all to me!" + # r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) + # self.assert200(status) + # self.assertPutResponse(r, "%s" % (rule_id)) + # self.assertTrue( + # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + # 'One or more of "amount", "duration", "name", "start" is missing.' + # ) + # self.assertTrue(r["name"] == "Give it all to me!") + # etag = r[self.app.config["ETAG"]] + # r, status = self.get( + # rule_url, "", + # r[self.domain["credit_rules"]["id_field"]] + # ) + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_put_dependency_fields_with_default(self): # Test that if a dependency is missing but has a default value then the # field is still accepted. See #353. From e3d2a3ddc4a7035f8672a2253e7f750394c641bb Mon Sep 17 00:00:00 2001 From: Henry Longmore Date: Sat, 28 Mar 2020 00:20:33 -0600 Subject: [PATCH 618/821] Add commented out patch test so tox passes --- eve/tests/methods/patch.py | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 317cc5b55..6046909e8 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -524,6 +524,52 @@ def test_patch_bandwidth_saver(self): db_value = self.compare_patch_with_get(self.app.config["ETAG"], r) self.assertEqual(db_value, r[self.app.config["ETAG"]]) + # def test_patch_bandwidth_saver_credit_rule_broken(self): + # _db = self.connection[MONGO_DBNAME] + # rule = { + # "amount": 300.0, + # "duration": "months", + # "name": "Testing BANDWIDTH_SAVER=False", + # "start": "2020-03-28T06:00:00 UTC", + # } + # rule_id = _db.credit_rules.insert_one(rule).inserted_id + # rule_url = "credit_rules/%s/" % (rule_id) + # changes = { + # "amount": 120.0, + # "duration": "months", + # "start": "2020-04-01T00:00:00 UTC", + # } + # response, _ = self.get("credit_rules/%s/" % (rule_id)) + # etag = response[ETAG] + # # bandwidth_saver is on by default + # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + # self.assertTrue(self.app.config["PROJECTION"]) + # r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) + # self.assert200(status) + # self.assertPatchResponse(r, "%s" % (rule_id)) + # self.assertFalse("amount" in r) + # etag = r[self.app.config["ETAG"]] + # r, _ = self.get(rule_url, "") + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + # + # # test return all fields (bandwidth_saver off) + # self.app.config["BANDWIDTH_SAVER"] = False + # changes["name"] = "Give it all to me!" + # r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) + # self.assert200(status) + # self.assertPatchResponse(r, "%s" % (rule_id)) + # self.assertTrue( + # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + # 'One or more of "amount", "duration", "name", "start" is missing.' + # ) + # self.assertTrue(r["name"] == "Give it all to me!") + # etag = r[self.app.config["ETAG"]] + # r, status = self.get( + # rule_url, "", + # r[self.domain["credit_rules"]["id_field"]] + # ) + # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_patch_readonly_field_with_previous_document(self): schema = self.domain["contacts"]["schema"] del schema["ref"]["required"] From 55821d1176e1df2ef42ef8f09eb5dafe831e8e55 Mon Sep 17 00:00:00 2001 From: Henry Longmore Date: Tue, 31 Mar 2020 03:23:46 -0600 Subject: [PATCH 619/821] Return entire document if BANDWIDTH_SAVER==False Fix for #1338 Uncommented and updated unit tests Updated CONTRIBUTING.rst --- CONTRIBUTING.rst | 6 ++- eve/methods/common.py | 5 +++ eve/tests/config.py | 10 +++-- eve/tests/methods/patch.py | 87 ++++++++++++++++++-------------------- eve/tests/methods/post.py | 76 ++++++++++++++++----------------- eve/tests/methods/put.py | 87 ++++++++++++++++++-------------------- 6 files changed, 138 insertions(+), 133 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a9a901bbd..9194dff8d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -141,7 +141,11 @@ order for the tests run. Save yourself some time and headache by creating a MongoDB user with the password defined in the `test_settings.py` file in the admin database (the pre-commit process is unforgiving if you don't want to commit your admin credentials but still have the file modified, which would be -necessary for tox). Also, be advised that in order to execute the +necessary for tox). If you want to run a local MongoDB instance along with an +SSH tunnel to a remote instance, if you can, have the local use the default +port and the remote use some other port. If you can't, fixing the tests that +won't play nicely is probably more trouble than connecting to the remote and +local instances one at a time. Also, be advised that in order to execute the :ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting tests are silently skipped if any of the two conditions are not met. diff --git a/eve/methods/common.py b/eve/methods/common.py index 4b65b2d8d..791f022d3 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -679,6 +679,11 @@ def resolve_resource_projection(document, resource): resource_def = config.DOMAIN[resource] projection = resource_def["datasource"]["projection"] + # Fix for #1338 + if not projection or not config.PROJECTION: + # BANDWIDTH_SAVER is disabled, and no projection is defined or + # projection feature is disabled, so return entire document. + return fields = { field for field, value in projection.items() if value and field in document } diff --git a/eve/tests/config.py b/eve/tests/config.py index 08d7f67f9..678b1ff3b 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -6,6 +6,10 @@ from eve.flaskapp import Eve from eve.io.base import DataLayer from eve.tests import TestBase +from eve.tests.test_settings import ( + MONGO_HOST, + MONGO_PORT, +) from eve.exceptions import ConfigException, SchemaException from eve.io.mongo import Mongo, Validator @@ -54,8 +58,8 @@ def test_default_settings(self): self.assertEqual(self.app.config["RATE_LIMIT_PATCH"], None) self.assertEqual(self.app.config["RATE_LIMIT_DELETE"], None) - self.assertEqual(self.app.config["MONGO_HOST"], "localhost") - self.assertEqual(self.app.config["MONGO_PORT"], 27017) + self.assertEqual(self.app.config["MONGO_HOST"], MONGO_HOST) + self.assertEqual(self.app.config["MONGO_PORT"], MONGO_PORT) self.assertEqual(self.app.config["MONGO_QUERY_BLACKLIST"], ["$where", "$regex"]) self.assertEqual(self.app.config["MONGO_QUERY_WHITELIST"], []) self.assertEqual(self.app.config["MONGO_WRITE_CONCERN"], {"w": 1}) @@ -494,7 +498,7 @@ def test_create_indexes(self): db_name = self.app.config["MONGO_DBNAME"] - db = MongoClient()[db_name] + db = MongoClient(host=MONGO_HOST, port=MONGO_PORT)[db_name] for coll in [db["mongodb_features"], db["mongodb_features_versions"]]: indexes = coll.index_information() diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 6046909e8..a2650d212 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -524,51 +524,48 @@ def test_patch_bandwidth_saver(self): db_value = self.compare_patch_with_get(self.app.config["ETAG"], r) self.assertEqual(db_value, r[self.app.config["ETAG"]]) - # def test_patch_bandwidth_saver_credit_rule_broken(self): - # _db = self.connection[MONGO_DBNAME] - # rule = { - # "amount": 300.0, - # "duration": "months", - # "name": "Testing BANDWIDTH_SAVER=False", - # "start": "2020-03-28T06:00:00 UTC", - # } - # rule_id = _db.credit_rules.insert_one(rule).inserted_id - # rule_url = "credit_rules/%s/" % (rule_id) - # changes = { - # "amount": 120.0, - # "duration": "months", - # "start": "2020-04-01T00:00:00 UTC", - # } - # response, _ = self.get("credit_rules/%s/" % (rule_id)) - # etag = response[ETAG] - # # bandwidth_saver is on by default - # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) - # self.assertTrue(self.app.config["PROJECTION"]) - # r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) - # self.assert200(status) - # self.assertPatchResponse(r, "%s" % (rule_id)) - # self.assertFalse("amount" in r) - # etag = r[self.app.config["ETAG"]] - # r, _ = self.get(rule_url, "") - # self.assertEqual(etag, r[self.app.config["ETAG"]]) - # - # # test return all fields (bandwidth_saver off) - # self.app.config["BANDWIDTH_SAVER"] = False - # changes["name"] = "Give it all to me!" - # r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) - # self.assert200(status) - # self.assertPatchResponse(r, "%s" % (rule_id)) - # self.assertTrue( - # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), - # 'One or more of "amount", "duration", "name", "start" is missing.' - # ) - # self.assertTrue(r["name"] == "Give it all to me!") - # etag = r[self.app.config["ETAG"]] - # r, status = self.get( - # rule_url, "", - # r[self.domain["credit_rules"]["id_field"]] - # ) - # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_patch_bandwidth_saver_credit_rule_broken(self): + _db = self.connection[MONGO_DBNAME] + rule = { + "amount": 300.0, + "duration": "months", + "name": "Testing BANDWIDTH_SAVER=False", + "start": "2020-03-28T06:00:00 UTC", + } + rule_id = _db.credit_rules.insert_one(rule).inserted_id + rule_url = "credit_rules/%s/" % (rule_id) + changes = { + "amount": 120.0, + "duration": "months", + "start": "2020-04-01T00:00:00 UTC", + } + response, _ = self.get("credit_rules/%s/" % (rule_id)) + etag = response[ETAG] + # bandwidth_saver is on by default + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + self.assertTrue(self.app.config["PROJECTION"]) + r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) + self.assert200(status) + self.assertPatchResponse(r, "%s" % (rule_id)) + self.assertFalse("amount" in r) + etag = r[self.app.config["ETAG"]] + r, _ = self.get(rule_url, "") + self.assertEqual(etag, r[self.app.config["ETAG"]]) + + # test return all fields (bandwidth_saver off) + self.app.config["BANDWIDTH_SAVER"] = False + changes["name"] = "Give it all to me!" + r, status = self.patch(rule_url, data=changes, headers=[("If-Match", etag)]) + self.assert200(status) + self.assertPatchResponse(r, "%s" % (rule_id)) + self.assertTrue( + all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + 'One or more of "amount", "duration", "name", "start" is missing.', + ) + self.assertTrue(r["name"] == "Give it all to me!") + etag = r[self.app.config["ETAG"]] + r, status = self.get(rule_url, "") + self.assertEqual(etag, r[self.app.config["ETAG"]]) def test_patch_readonly_field_with_previous_document(self): schema = self.domain["contacts"]["schema"] diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index bccec352c..461705f89 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -657,45 +657,43 @@ def test_post_bandwidth_saver(self): ) self.assertEqual(etag, r[self.app.config["ETAG"]]) - # def test_post_bandwidth_saver_credit_rule_broken(self): - # data = [ - # { - # "amount": 300.0, - # "duration": "months", - # "name": "Bandwidth Saver:True, Projection:True", - # "start": "2020-03-28T06:00:00 UTC" - # } - # ] - # - # # bandwidth_saver is on by default - # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) - # self.assertTrue(self.app.config["PROJECTION"]) - # r, status = self.post("credit_rules", data=data) - # self.assert201(status) - # self.assertPostResponse(r) - # self.assertFalse("amount" in r) - # etag = r[self.app.config["ETAG"]] - # r, status = self.get( - # "credit_rules", "", - # r[self.domain["credit_rules"]["id_field"]] - # ) - # self.assertEqual(etag, r[self.app.config["ETAG"]]) - # - # # test return all fields (bandwidth_saver off) - # self.app.config["BANDWIDTH_SAVER"] = False - # r, status = self.post("credit_rules", data=data) - # self.assert201(status) - # self.assertPostResponse(r) - # self.assertTrue( - # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), - # 'One or more of "amount", "duration", "name", "start" is missing.' - # ) - # etag = r[self.app.config["ETAG"]] - # r, status = self.get( - # "credit_rules", "", - # r[self.domain["credit_rules"]["id_field"]] - # ) - # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_post_bandwidth_saver_credit_rule_broken(self): + data = [ + { + "amount": 300.0, + "duration": "months", + "name": "Bandwidth Saver:True, Projection:True", + "start": "2020-03-28T06:00:00 UTC", + } + ] + + # bandwidth_saver is on by default + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + self.assertTrue(self.app.config["PROJECTION"]) + r, status = self.post("credit_rules", data=data) + self.assert201(status) + self.assertPostResponse(r) + self.assertFalse("amount" in r) + etag = r[self.app.config["ETAG"]] + r, status = self.get( + "credit_rules", "", r[self.domain["credit_rules"]["id_field"]] + ) + self.assertEqual(etag, r[self.app.config["ETAG"]]) + + # test return all fields (bandwidth_saver off) + self.app.config["BANDWIDTH_SAVER"] = False + r, status = self.post("credit_rules", data=data) + self.assert201(status) + self.assertPostResponse(r) + self.assertTrue( + all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + 'One or more of "amount", "duration", "name", "start" is missing.', + ) + etag = r[self.app.config["ETAG"]] + r, status = self.get( + "credit_rules", "", r[self.domain["credit_rules"]["id_field"]] + ) + self.assertEqual(etag, r[self.app.config["ETAG"]]) def test_post_alternative_payload(self): payl = {"ref": "5432112345678901234567890", "role": ["agent"]} diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index 2358181a1..c00e4bd09 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -362,51 +362,48 @@ def test_put_bandwidth_saver(self): db_value = self.compare_put_with_get(self.app.config["ETAG"], r) self.assertEqual(db_value, r[self.app.config["ETAG"]]) - # def test_put_bandwidth_saver_credit_rule_broken(self): - # _db = self.connection[MONGO_DBNAME] - # rule = { - # "amount": 300.0, - # "duration": "months", - # "name": "Testing BANDWIDTH_SAVER=False", - # "start": "2020-03-28T06:00:00 UTC", - # } - # rule_id = _db.credit_rules.insert_one(rule).inserted_id - # rule_url = "credit_rules/%s/" % (rule_id) - # changes = { - # "amount": 120.0, - # "duration": "months", - # "start": "2020-04-01T00:00:00 UTC", - # } - # response, _ = self.get("credit_rules/%s/" % (rule_id)) - # etag = response[ETAG] - # # bandwidth_saver is on by default - # self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) - # self.assertTrue(self.app.config["PROJECTION"]) - # r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) - # self.assert200(status) - # self.assertPutResponse(r, "%s" % (rule_id)) - # self.assertFalse("amount" in r) - # etag = r[self.app.config["ETAG"]] - # r, _ = self.get(rule_url, "") - # self.assertEqual(etag, r[self.app.config["ETAG"]]) - # - # # test return all fields (bandwidth_saver off) - # self.app.config["BANDWIDTH_SAVER"] = False - # changes["name"] = "Give it all to me!" - # r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) - # self.assert200(status) - # self.assertPutResponse(r, "%s" % (rule_id)) - # self.assertTrue( - # all(["amount" in r, "duration" in r, "name" in r, "start" in r]), - # 'One or more of "amount", "duration", "name", "start" is missing.' - # ) - # self.assertTrue(r["name"] == "Give it all to me!") - # etag = r[self.app.config["ETAG"]] - # r, status = self.get( - # rule_url, "", - # r[self.domain["credit_rules"]["id_field"]] - # ) - # self.assertEqual(etag, r[self.app.config["ETAG"]]) + def test_put_bandwidth_saver_credit_rule_broken(self): + _db = self.connection[MONGO_DBNAME] + rule = { + "amount": 300.0, + "duration": "months", + "name": "Testing BANDWIDTH_SAVER=False", + "start": "2020-03-28T06:00:00 UTC", + } + rule_id = _db.credit_rules.insert_one(rule).inserted_id + rule_url = "credit_rules/%s/" % (rule_id) + changes = { + "amount": 120.0, + "duration": "months", + "start": "2020-04-01T00:00:00 UTC", + } + response, _ = self.get("credit_rules/%s/" % (rule_id)) + etag = response[ETAG] + # bandwidth_saver is on by default + self.assertTrue(self.app.config["BANDWIDTH_SAVER"]) + self.assertTrue(self.app.config["PROJECTION"]) + r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) + self.assert200(status) + self.assertPutResponse(r, "%s" % (rule_id)) + self.assertFalse("amount" in r) + etag = r[self.app.config["ETAG"]] + r, _ = self.get(rule_url, "") + self.assertEqual(etag, r[self.app.config["ETAG"]]) + + # test return all fields (bandwidth_saver off) + self.app.config["BANDWIDTH_SAVER"] = False + changes["name"] = "Give it all to me!" + r, status = self.put(rule_url, data=changes, headers=[("If-Match", etag)]) + self.assert200(status) + self.assertPutResponse(r, "%s" % (rule_id)) + self.assertTrue( + all(["amount" in r, "duration" in r, "name" in r, "start" in r]), + 'One or more of "amount", "duration", "name", "start" is missing.', + ) + self.assertTrue(r["name"] == "Give it all to me!") + etag = r[self.app.config["ETAG"]] + r, status = self.get(rule_url, "") + self.assertEqual(etag, r[self.app.config["ETAG"]]) def test_put_dependency_fields_with_default(self): # Test that if a dependency is missing but has a default value then the From af340edf19ff16244f1e049d46b2bb31982fcef7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Apr 2020 09:41:57 +0200 Subject: [PATCH 620/821] Changelog for #1374 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d7f661ef5..e9493762f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,11 +9,13 @@ In Development Fixed ~~~~~ -- Fix ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) +- ``BANDWIDTH_SAVER`` no longer works with resolve_resource_projection (`#1338`_) +- ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) - dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) +.. _`#1338`: https://github.com/pyeve/eve/issues/1338 .. _`#1368`: https://github.com/pyeve/eve/pull/1368 .. _`#1366`: https://github.com/pyeve/eve/pull/1366 .. _`#1362`: https://github.com/pyeve/eve/pull/1362 From 4fb864b9e2aa5a84e17712fab4de2ce627cceb7b Mon Sep 17 00:00:00 2001 From: Michael Maxwell Date: Thu, 2 Apr 2020 05:06:18 -0700 Subject: [PATCH 621/821] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e9493762f..743618029 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,7 +35,7 @@ New Fixed ~~~~~ - Starup crash with Werkzeug 1.0 (`#1359`_) -- ``$eq`` is missing from supported query opeators (`#1351`_) +- ``$eq`` is missing from supported query operators (`#1351`_) - Documentation typos (`#1348`_, `#1350`_) .. _`#1359`: https://github.com/pyeve/eve/issues/1359 From ba1b2c32d099fe1d5d46b086176aaadac4ffab83 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Apr 2020 09:52:28 +0200 Subject: [PATCH 622/821] Changelog for #1375 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 743618029..5a307a639 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,7 +14,9 @@ Fixed - dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) +- Documentation typos (`#1375`_) +.. _`#1375`: https://github.com/pyeve/eve/pull/1375 .. _`#1338`: https://github.com/pyeve/eve/issues/1338 .. _`#1368`: https://github.com/pyeve/eve/pull/1368 .. _`#1366`: https://github.com/pyeve/eve/pull/1366 From e1d989f0eb6ce99c15b2de588772ea53267603a0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Apr 2020 09:53:30 +0200 Subject: [PATCH 623/821] Michael Maxwell --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 7bf5eb994..81eb09528 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,7 +9,6 @@ Development Lead Patches and Contributions ````````````````````````` - - Aayush Sarva - Adam Walsh - Alberto Marin @@ -126,6 +125,7 @@ Patches and Contributions - Matthieu Prat - Mattias Lundberg - Mayur Dhamanwala +- Michael Maxwell - Mikael Berg - Moritz Schneider - Moritz Schneider From a1054fdd22d798ae5a4f2b2188552a6d00d4521d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 4 Apr 2020 09:59:07 +0200 Subject: [PATCH 624/821] black styling --- eve/tests/config.py | 5 +---- eve/tests/endpoints.py | 6 +----- eve/tests/test_settings.py | 4 ++-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/eve/tests/config.py b/eve/tests/config.py index 678b1ff3b..8e9b7c87f 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -6,10 +6,7 @@ from eve.flaskapp import Eve from eve.io.base import DataLayer from eve.tests import TestBase -from eve.tests.test_settings import ( - MONGO_HOST, - MONGO_PORT, -) +from eve.tests.test_settings import MONGO_HOST, MONGO_PORT from eve.exceptions import ConfigException, SchemaException from eve.io.mongo import Mongo, Validator diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index ec17598e6..03886deca 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -7,11 +7,7 @@ from datetime import datetime from eve.utils import config from eve.io.base import BaseJSONEncoder -from eve.tests.test_settings import ( - MONGO_DBNAME, - MONGO_USERNAME, - MONGO_PASSWORD, -) +from eve.tests.test_settings import MONGO_DBNAME, MONGO_USERNAME, MONGO_PASSWORD from uuid import UUID from eve.io.mongo import Validator import os diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 3f07fedec..5dae1f5e3 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -325,8 +325,8 @@ "type": "string", "allowed": ["days", "weeks", "months", "years"], "required": False, - } - } + }, + }, } child_products = copy.deepcopy(products) From fc7145af3c7d5c1dd607d92c0a9d2cf64c7e63bb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 5 Apr 2020 17:22:46 +0200 Subject: [PATCH 625/821] pin flask to 1.1.1 or previous Closes #1376 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f5e8f71b..e45b19d0c 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ INSTALL_REQUIRES = [ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", - "flask>=1.0", + "flask<=1.1.1", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", ] From 6d4637928c147f7610703903fc5255121ba7fb9e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 19 Apr 2020 09:24:12 +0200 Subject: [PATCH 626/821] Fix for #1378 Flask 1.1.2 changed its behavior around static endpoints. Now, an eventual forward slash ('/') at the end of the url will be stripped out. See pallets/flask#3452 --- CHANGES.rst | 2 ++ eve/tests/config.py | 4 ++-- setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5a307a639..2eca3246c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ In Development Fixed ~~~~~ +- Tests failing with Flask 1.1.2 (`#1378`_) - ``BANDWIDTH_SAVER`` no longer works with resolve_resource_projection (`#1338`_) - ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) - dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) @@ -16,6 +17,7 @@ Fixed - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) - Documentation typos (`#1375`_) +.. _`#1378`: https://github.com/pyeve/eve/pull/1378 .. _`#1375`: https://github.com/pyeve/eve/pull/1375 .. _`#1338`: https://github.com/pyeve/eve/issues/1338 .. _`#1368`: https://github.com/pyeve/eve/pull/1368 diff --git a/eve/tests/config.py b/eve/tests/config.py index 8e9b7c87f..cc7f9cd00 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -33,8 +33,8 @@ def test_custom_import_name(self): self.assertEqual(self.app.import_name, "unittest") def test_custom_kwargs(self): - self.app = Eve("unittest", static_folder="/", settings=self.settings_file) - self.assertEqual(self.app.static_folder, "/") + self.app = Eve("unittest", static_folder="static/", settings=self.settings_file) + self.assertTrue(self.app.static_folder.endswith("static")) def test_regexconverter(self): regex_converter = self.app.url_map.converters.get("regex") diff --git a/setup.py b/setup.py index e45b19d0c..e6b7ac007 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ INSTALL_REQUIRES = [ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", - "flask<=1.1.1", + "flask", "pymongo>=3.7", "simplejson>=3.3.0,<4.0", ] From 0c70f1d077d111d161896f87c7b4ff3a772db964 Mon Sep 17 00:00:00 2001 From: Petr Jasek Date: Thu, 7 May 2020 14:46:54 +0200 Subject: [PATCH 627/821] fix versioning on PATCH when `merge_nested_documents` is disabled fix #1388 --- eve/methods/patch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 3ad9d26b8..ec914eb69 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -212,7 +212,8 @@ def patch_internal( if resource_def["merge_nested_documents"]: updates = resolve_nested_documents(updates, updated) - updated.update(updates) + + updated.update(updates) if config.IF_MATCH: resolve_document_etag(updated, resource) From 897683ed13c802847f17d90887032f009f646baa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 May 2020 09:02:09 +0200 Subject: [PATCH 628/821] changelog for #1389 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2eca3246c..1e21a82c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ In Development Fixed ~~~~~ +- Disabling ``merge_nested_documents`` breaks versioning on PATCH (`#1389`_) - Tests failing with Flask 1.1.2 (`#1378`_) - ``BANDWIDTH_SAVER`` no longer works with resolve_resource_projection (`#1338`_) - ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) @@ -17,6 +18,7 @@ Fixed - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) - Documentation typos (`#1375`_) +.. _`#1389`: https://github.com/pyeve/eve/issues/1389 .. _`#1378`: https://github.com/pyeve/eve/pull/1378 .. _`#1375`: https://github.com/pyeve/eve/pull/1375 .. _`#1338`: https://github.com/pyeve/eve/issues/1338 From 20b1e937613685c7343c6f0f54574cf51286a98d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 May 2020 09:09:34 +0200 Subject: [PATCH 629/821] changelog fix --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e21a82c1..8bacb4ac4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,7 +11,7 @@ Fixed - Disabling ``merge_nested_documents`` breaks versioning on PATCH (`#1389`_) - Tests failing with Flask 1.1.2 (`#1378`_) -- ``BANDWIDTH_SAVER`` no longer works with resolve_resource_projection (`#1338`_) +- ``BANDWIDTH_SAVER`` no longer works with ``resolve_resource_projection`` (`#1338`_) - ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) - dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) From 53b725a66618f933d1e1c72b0d4698d9c9af4d24 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 May 2020 09:10:52 +0200 Subject: [PATCH 630/821] bump version to 1.1.1 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8bacb4ac4..e1d966a32 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 1.1.1 +------------- + +Released on May 10, 2020. + Fixed ~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index 192108eca..2fb90808a 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.1.dev0" +__version__ = "1.1.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 342e0ae03ee8f5dde5855111485d0d37362d6a08 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 10 May 2020 09:22:50 +0200 Subject: [PATCH 631/821] bump version to 1.1.2.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 2fb90808a..397b646f8 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.1" +__version__ = "1.1.2.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 312eeb3d2e23c1fce12d0b0f12d63d11ee0b05cd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 12 May 2020 18:55:42 +0200 Subject: [PATCH 632/821] typo. Thanks @lig1 for reporting it. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e1d966a32..365e0a4fc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Fixed - Tests failing with Flask 1.1.2 (`#1378`_) - ``BANDWIDTH_SAVER`` no longer works with ``resolve_resource_projection`` (`#1338`_) - ``unique_within_resource`` rule used in resources without datasource filter (`#1368`_) -- dics without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) +- dicts without ``schema`` rule are broken since ``b8d8fcd`` (`#1366`_) - 403 Forrbidden added to ``STANDARD_ERRORS`` (`#1362`_) - ``unique`` constraint doesn't work when inside of a dict or a list (`#1360`_) - Documentation typos (`#1375`_) From 58cbe0f1d19ceb86463a159d1b893219d067dc6d Mon Sep 17 00:00:00 2001 From: Prajjwal Nijhara Date: Mon, 11 May 2020 20:02:51 +0530 Subject: [PATCH 633/821] Remove unnecessary comprehension --- eve/render.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/eve/render.py b/eve/render.py index 9662ebb1e..bc539eb7c 100644 --- a/eve/render.py +++ b/eve/render.py @@ -446,11 +446,9 @@ def xml_add_links(cls, data): data.update({config.LINKS: {rel: link}}) elif isinstance(link, list): - xml += "".join( - [ - chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) - for d in link - ] + xml += "".join( + chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) + for d in link ) else: xml += "".join(chunk % (rel, utils.escape(link["href"]), link["title"])) @@ -467,7 +465,7 @@ def xml_add_items(cls, data): .. versionadded:: 0.0.3 """ try: - xml = "".join([cls.xml_item(item) for item in data[config.ITEMS]]) + xml = "".join(cls.xml_item(item) for item in data[config.ITEMS]) except: xml = cls.xml_dict(data) return xml From f90ed01478110c5003cbb281c35001c05cd4af69 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 16 May 2020 11:02:41 +0200 Subject: [PATCH 634/821] changelog for #1391 --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 365e0a4fc..8793b34b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +Fixed +~~~~~ + +- Removed unnecessary comprehension (`#1391`_) + +.. _`#1391`: https://github.com/pyeve/eve/pull/1391 Version 1.1.1 ------------- From 358214200709f6ac5361b5100c540d33f4993ffa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 16 May 2020 11:02:53 +0200 Subject: [PATCH 635/821] Prajjwal Nijhara --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 81eb09528..961fe5858 100644 --- a/AUTHORS +++ b/AUTHORS @@ -149,6 +149,7 @@ Patches and Contributions - Peter Darrow - Petr Jašek - Phone Myint Kyaw +- Prajjwal Nijhara - Prayag Verma - Qiang Zhang - Ralph Smith From f434d56485190ef029b1c49bb909294690f30026 Mon Sep 17 00:00:00 2001 From: ride90 Date: Thu, 4 Jun 2020 15:11:32 +0200 Subject: [PATCH 636/821] Use resource projection flag --- eve/methods/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 791f022d3..9cb06bd9f 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -679,8 +679,9 @@ def resolve_resource_projection(document, resource): resource_def = config.DOMAIN[resource] projection = resource_def["datasource"]["projection"] + projection_enabled = resource_def["projection"] # Fix for #1338 - if not projection or not config.PROJECTION: + if not projection_enabled or not projection: # BANDWIDTH_SAVER is disabled, and no projection is defined or # projection feature is disabled, so return entire document. return From 7d3237ee9458402ff2ea8891ad06a808454dcd90 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 9 Jul 2020 18:44:44 +0200 Subject: [PATCH 637/821] Oleg Pshenichniy --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 961fe5858..8636c7c1b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Patches and Contributions - Nick Park - Nicolas Bazire - Nicolas Carlier +- Oleg Pshenichniy - Olivier Carrère - Olivier Poitrey - Olof Johansson From b1d5f26b4f0c9f3d3aa7c0e15b22122a51d75a24 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 9 Jul 2020 18:47:00 +0200 Subject: [PATCH 638/821] Changelog for #1398 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8793b34b4..bdfd9ed05 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,8 +9,10 @@ In Development Fixed ~~~~~ +- Add missed condition when projection is disabled per domain (`#1398`_) - Removed unnecessary comprehension (`#1391`_) +.. _`#1398`: https://github.com/pyeve/eve/pull/1398 .. _`#1391`: https://github.com/pyeve/eve/pull/1391 Version 1.1.1 From c7448e13323e3b4a8897eb4c1c402337c50235cc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 9 Jul 2020 18:50:55 +0200 Subject: [PATCH 639/821] Bump version to 1.1.2 --- CHANGES.rst | 5 +++++ eve/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bdfd9ed05..598497486 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +Version 1.1.2 +------------- + +Released on July 9, 2020. + Fixed ~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index 397b646f8..f4e730adf 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.2.dev0" +__version__ = "1.1.2" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From b174c7dcb1e93151daadc08948a387e2dd4b0328 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 9 Jul 2020 18:56:12 +0200 Subject: [PATCH 640/821] bump version to 1.1.3.devo0 --- CHANGES.rst | 2 ++ eve/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 598497486..57765e5b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + Version 1.1.2 ------------- diff --git a/eve/__init__.py b/eve/__init__.py index f4e730adf..b5a77c963 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.2" +__version__ = "1.1.3.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 0c97071d1f7194ddca6c2919d39cc41433c0aa94 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Fri, 18 Sep 2020 16:11:39 +0200 Subject: [PATCH 641/821] Allow mongo_options to be passed to post, patch, and get_document. This is so users can control ReadPreference and WriteConcern. --- eve/methods/common.py | 13 ++++++++++--- eve/methods/patch.py | 10 +++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 9cb06bd9f..0d548fdb7 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -40,6 +40,7 @@ def get_document( original=None, check_auth_value=True, force_auth_field_projection=False, + mongo_options=None, **lookup ): """ Retrieves and return a single document. Since this function is used by @@ -60,6 +61,7 @@ def get_document( the user-restricted resource access field (if configured). Defaults to ``False``. + :param mongo_options: Options to pass to PyMongo. e.g. ReadConcern :param **lookup: document lookup query .. versionchanged:: 0.6 @@ -85,9 +87,14 @@ def get_document( if original: document = original else: - document = app.data.find_one( - resource, req, check_auth_value, force_auth_field_projection, **lookup - ) + if mongo_options: + document = app.data.with_options(mongo_options).find_one( + resource, req, check_auth_value, force_auth_field_projection, **lookup + ) + else: + document = app.data.find_one( + resource, req, check_auth_value, force_auth_field_projection, **lookup + ) if document: e_if_m = config.ENFORCE_IF_MATCH diff --git a/eve/methods/patch.py b/eve/methods/patch.py index ec914eb69..3ee420775 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -55,7 +55,7 @@ def patch(resource, payload=None, **lookup): def patch_internal( - resource, payload=None, concurrency_check=False, skip_validation=False, **lookup + resource, payload=None, concurrency_check=False, skip_validation=False, mongo_options=None, **lookup ): """ Intended for internal patch calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and @@ -75,6 +75,7 @@ def patch_internal( option, a request context must be available. :param concurrency_check: concurrency check switch (bool) :param skip_validation: skip payload validation before write (bool) + :param mongo_options: options to pass to PyMongo. e.g. ReadConcern of the initial get. :param **lookup: document lookup query. .. versionchanged:: 0.6.2 @@ -145,7 +146,7 @@ def patch_internal( if payload is None: payload = payload_() - original = get_document(resource, concurrency_check, **lookup) + original = get_document(resource, concurrency_check, mongo_options, **lookup) if not original: # not found abort(404) @@ -213,7 +214,10 @@ def patch_internal( if resource_def["merge_nested_documents"]: updates = resolve_nested_documents(updates, updated) - updated.update(updates) + if mongo_options: + updated.with_options(mongo_options).update(updates) + else: + updated.update(updates) if config.IF_MATCH: resolve_document_etag(updated, resource) From 29eb22e2ea6f06a02c425f538e0df189658fafa3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 19 Sep 2020 09:41:21 +0200 Subject: [PATCH 642/821] changelog for #1412 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 57765e5b9..3e7cc154e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Fix: Race condition in PATCH on newly created documents with clustered mongo (`#1411`_) + +.. _`#1411`: https://github.com/pyeve/eve/issues/1411 Version 1.1.2 ------------- From 1ac8d32858c87f0ebc9d02c326ce70556518d4a8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 19 Sep 2020 09:48:04 +0200 Subject: [PATCH 643/821] bump version to 0.1.3 --- CHANGES.rst | 10 ++++++++++ eve/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3e7cc154e..055d64e09 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,16 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 1.1.3 +------------- + +Released on September 19, 2020. + +Fixed +~~~~~ + - Fix: Race condition in PATCH on newly created documents with clustered mongo (`#1411`_) .. _`#1411`: https://github.com/pyeve/eve/issues/1411 diff --git a/eve/__init__.py b/eve/__init__.py index b5a77c963..0a90a588a 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.3.dev0" +__version__ = "1.1.3" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 2ded37c647820b30e3ffd4673e8539f666a4e192 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 19 Sep 2020 09:58:13 +0200 Subject: [PATCH 644/821] fix linting --- eve/tests/logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eve/tests/logging.py b/eve/tests/logging.py index 59442cd69..31cdf5c3f 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -9,12 +9,12 @@ class TestUtils(TestBase): """ @log_capture() - def test_logging_info(self, l): + def test_logging_info(self, log): self.app.logger.propagate = True self.app.logger.info("test info") - l.check(("eve", "INFO", "test info")) + log.check(("eve", "INFO", "test info")) - log_record = l.records[0] + log_record = log.records[0] self.assertEqual(log_record.clientip, None) self.assertEqual(log_record.method, None) self.assertEqual(log_record.url, None) From 6da848a203b6c8b086b5c996e792d0edf45b25ed Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 19 Sep 2020 10:13:39 +0200 Subject: [PATCH 645/821] black magic --- eve/auth.py | 26 ++++---- eve/endpoints.py | 14 ++--- eve/exceptions.py | 2 +- eve/flaskapp.py | 54 ++++++++-------- eve/io/base.py | 42 ++++++------- eve/io/media.py | 10 +-- eve/io/mongo/media.py | 15 +++-- eve/io/mongo/mongo.py | 56 ++++++++--------- eve/io/mongo/parser.py | 30 ++++----- eve/io/mongo/validation.py | 22 +++---- eve/logging.py | 2 +- eve/methods/common.py | 120 ++++++++++++++++++------------------ eve/methods/delete.py | 4 +- eve/methods/get.py | 8 +-- eve/methods/patch.py | 11 +++- eve/methods/put.py | 2 +- eve/render.py | 44 ++++++------- eve/tests/__init__.py | 4 +- eve/tests/auth.py | 15 +++-- eve/tests/config.py | 2 +- eve/tests/endpoints.py | 4 +- eve/tests/io/media.py | 4 +- eve/tests/io/mongo.py | 8 +-- eve/tests/logging.py | 2 +- eve/tests/methods/common.py | 8 +-- eve/tests/methods/delete.py | 12 ++-- eve/tests/methods/get.py | 40 ++++++------ eve/tests/methods/patch.py | 20 +++--- eve/tests/methods/post.py | 4 +- eve/tests/methods/put.py | 22 +++---- eve/tests/renders.py | 3 +- eve/tests/utils.py | 2 +- eve/tests/versioning.py | 106 +++++++++++++++---------------- eve/utils.py | 34 +++++----- eve/validation.py | 14 ++--- eve/versioning.py | 20 +++--- 36 files changed, 378 insertions(+), 408 deletions(-) diff --git a/eve/auth.py b/eve/auth.py index 29a29d2f4..25a43da95 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -14,7 +14,7 @@ def requires_auth(endpoint_class): - """ Enables Authorization logic for decorated functions. + """Enables Authorization logic for decorated functions. :param endpoint_class: the 'class' to which the decorated endpoint belongs to. Can be 'resource' (resource endpoint), 'item' @@ -85,7 +85,7 @@ def decorated(*args, **kwargs): class BasicAuth(object): - """ Implements Basic AUTH logic. Should be subclassed to implement custom + """Implements Basic AUTH logic. Should be subclassed to implement custom authentication checking. .. versionchanged:: 0.7 @@ -131,7 +131,7 @@ def set_user_or_token(self, user): g.user = user def check_auth(self, username, password, allowed_roles, resource, method): - """ This function is called to check if a username / password + """This function is called to check if a username / password combination is valid. Must be overridden with custom logic. :param username: username provided with current request. @@ -143,7 +143,7 @@ def check_auth(self, username, password, allowed_roles, resource, method): raise NotImplementedError def authenticate(self): - """ Returns a standard a 401 response that enables basic auth. + """Returns a standard a 401 response that enables basic auth. Override if you want to change the response and/or the realm. """ abort( @@ -153,7 +153,7 @@ def authenticate(self): ) def authorized(self, allowed_roles, resource, method): - """ Validates the the current request is allowed to pass through. + """Validates the the current request is allowed to pass through. :param allowed_roles: allowed roles for the current request, can be a string or a list of roles. @@ -168,7 +168,7 @@ def authorized(self, allowed_roles, resource, method): class HMACAuth(BasicAuth): - """ Hash Message Authentication Code (HMAC) authentication logic. Must be + """Hash Message Authentication Code (HMAC) authentication logic. Must be subclassed to implement custom authorization checking. .. versionchanged:: 0.7 @@ -190,7 +190,7 @@ class HMACAuth(BasicAuth): def check_auth( self, userid, hmac_hash, headers, data, allowed_roles, resource, method ): - """ This function is called to check if a token is valid. Must be + """This function is called to check if a token is valid. Must be overridden with custom logic. :param userid: user id included with the request. @@ -204,7 +204,7 @@ def check_auth( raise NotImplementedError def authorized(self, allowed_roles, resource, method): - """ Validates the the current request is allowed to pass through. + """Validates the the current request is allowed to pass through. :param allowed_roles: allowed roles for the current request, can be a string or a list of roles. @@ -228,7 +228,7 @@ def authorized(self, allowed_roles, resource, method): class TokenAuth(BasicAuth): - """ Implements Token AUTH logic. Should be subclassed to implement custom + """Implements Token AUTH logic. Should be subclassed to implement custom authentication checking. .. versionchanged:: 0.7 @@ -245,7 +245,7 @@ class TokenAuth(BasicAuth): """ def check_auth(self, token, allowed_roles, resource, method): - """ This function is called to check if a token is valid. Must be + """This function is called to check if a token is valid. Must be overridden with custom logic. :param token: decoded user name. @@ -256,7 +256,7 @@ def check_auth(self, token, allowed_roles, resource, method): raise NotImplementedError def authorized(self, allowed_roles, resource, method): - """ Validates the the current request is allowed to pass through. + """Validates the the current request is allowed to pass through. :param allowed_roles: allowed roles for the current request, can be a string or a list of roles. @@ -282,7 +282,7 @@ def authorized(self, allowed_roles, resource, method): def auth_field_and_value(resource): - """ If auth is active and the resource requires it, return both the + """If auth is active and the resource requires it, return both the current request 'request_auth_value' and the 'auth_field' for the resource .. versionchanged:: 0.4 @@ -313,7 +313,7 @@ def auth_field_and_value(resource): def resource_auth(resource): - """ Ensure resource auth is an instance and its state is preserved between + """Ensure resource auth is an instance and its state is preserved between calls. .. versionchanged:: 0.6 diff --git a/eve/endpoints.py b/eve/endpoints.py index 91a20c8f4..d73ca1fab 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -25,7 +25,7 @@ def collections_endpoint(**lookup): - """ Resource endpoint handler + """Resource endpoint handler :param url: the url that led here @@ -66,7 +66,7 @@ def collections_endpoint(**lookup): def item_endpoint(**lookup): - """ Item endpoint handler + """Item endpoint handler :param url: the url that led here :param lookup: sub resource query @@ -108,7 +108,7 @@ def item_endpoint(**lookup): @ratelimit() @requires_auth("home") def home_endpoint(): - """ Home/API entry point. Will provide links to each available resource + """Home/API entry point. Will provide links to each available resource .. versionchanged:: 0.5 Resource URLs are relative to API root. @@ -159,7 +159,7 @@ def home_endpoint(): def error_endpoint(error): - """ Response returned when an error is raised by the API (e.g. my means of + """Response returned when an error is raised by the API (e.g. my means of an abort(4xx). """ headers = [] @@ -188,7 +188,7 @@ def _resource(): @requires_auth("media") def media_endpoint(_id): - """ This endpoint is active when RETURN_MEDIA_AS_URL is True. It retrieves + """This endpoint is active when RETURN_MEDIA_AS_URL is True. It retrieves a media file and streams it to the client. .. versionadded:: 0.6 @@ -254,7 +254,7 @@ def media_endpoint(_id): @requires_auth("resource") def schema_item_endpoint(resource): - """ This endpoint is active when SCHEMA_ENDPOINT != None. It returns the + """This endpoint is active when SCHEMA_ENDPOINT != None. It returns the requested resource's schema definition in JSON format. """ resource_config = app.config["DOMAIN"].get(resource) @@ -266,7 +266,7 @@ def schema_item_endpoint(resource): @requires_auth("home") def schema_collection_endpoint(): - """ This endpoint is active when SCHEMA_ENDPOINT != None. It returns the + """This endpoint is active when SCHEMA_ENDPOINT != None. It returns the schema definition for all public or request authenticated resources in JSON format. """ diff --git a/eve/exceptions.py b/eve/exceptions.py index 230443f8f..34a561fdc 100644 --- a/eve/exceptions.py +++ b/eve/exceptions.py @@ -12,7 +12,7 @@ class ConfigException(Exception): - """ Raised when errors are found in the configuration settings (usually + """Raised when errors are found in the configuration settings (usually `settings.py`). """ diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 85be744a1..36dfd0d64 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -38,7 +38,7 @@ class EveWSGIRequestHandler(WSGIRequestHandler): - """ Extend werkzeug request handler to include current Eve version in all + """Extend werkzeug request handler to include current Eve version in all responses, which is super-handy for debugging. """ @@ -59,7 +59,7 @@ def __init__(self, url_map, *items): class Eve(Flask, Events): - """ The main Eve object. On initialization it will load Eve settings, then + """The main Eve object. On initialization it will load Eve settings, then configure and enable the API endpoints. The API is launched by executing the code below::: @@ -144,7 +144,7 @@ def __init__( media=GridFSMediaStorage, **kwargs ): - """ Eve main WSGI app is implemented as a Flask subclass. Since we want + """Eve main WSGI app is implemented as a Flask subclass. Since we want to be able to launch our API by simply invoking Flask's run() method, we need to enhance our super-class a little bit. """ @@ -216,13 +216,13 @@ def run(self, host=None, port=None, debug=None, **options): :param options: the options to be forwarded to the underlying Werkzeug server. See :func:`werkzeug.serving.run_simple` for more - information. """ + information.""" options.setdefault("request_handler", EveWSGIRequestHandler) super(Eve, self).run(host, port, debug, **options) def load_config(self): - """ API settings are loaded from standard python modules. First from + """API settings are loaded from standard python modules. First from `settings.py`(or alternative name/path passed as an argument) and then, when defined, from the file specified in the `EVE_SETTINGS` environment variable. @@ -284,11 +284,10 @@ def find_settings_file(file_name): self.check_deprecated_features() def check_deprecated_features(self): - """ Method checks for usage of deprecated features. - """ + """Method checks for usage of deprecated features.""" def deprecated_renderers_settings(): - """ Checks if JSON or XML setting is still being used instead of + """Checks if JSON or XML setting is still being used instead of RENDERERS and if so, composes new settings. """ msg = ( @@ -312,7 +311,7 @@ def deprecated_renderers_settings(): deprecated_renderers_settings() def validate_domain_struct(self): - """ Validates that Eve configuration settings conform to the + """Validates that Eve configuration settings conform to the requirements. """ try: @@ -323,7 +322,7 @@ def validate_domain_struct(self): raise ConfigException("DOMAIN must be a dict.") def validate_config(self): - """ Makes sure that REST methods expressed in the configuration + """Makes sure that REST methods expressed in the configuration settings are supported. .. versionchanged:: 0.2.0 @@ -356,7 +355,7 @@ def validate_config(self): self._validate_resource_settings(resource, settings) def _validate_resource_settings(self, resource, settings): - """ Validates one resource in configuration settings. + """Validates one resource in configuration settings. :param resource: name of the resource which settings refer to. :param settings: settings of resource to be validated. @@ -406,7 +405,7 @@ def _validate_resource_settings(self, resource, settings): self.validate_schema(resource, settings["schema"]) def validate_roles(self, directive, candidate, resource): - """ Validates that user role directives are syntactically and formally + """Validates that user role directives are syntactically and formally adequate. :param directive: either 'allowed_[read_|write_]roles' or @@ -422,7 +421,7 @@ def validate_roles(self, directive, candidate, resource): raise ConfigException("'%s' must be list" "[%s]." % (directive, resource)) def validate_methods(self, allowed, proposed, item): - """ Compares allowed and proposed methods, raising a `ConfigException` + """Compares allowed and proposed methods, raising a `ConfigException` when they don't match. :param allowed: a list of supported (allowed) methods. @@ -438,7 +437,7 @@ def validate_methods(self, allowed, proposed, item): ) def validate_schema(self, resource, schema): - """ Validates a resource schema. + """Validates a resource schema. :param resource: resource name. :param schema: schema definition for the resource. @@ -538,7 +537,7 @@ def validate_field_name(field): # TODO are there other mandatory settings? Validate them here def set_defaults(self): - """ When not provided, fills individual resource settings with default + """When not provided, fills individual resource settings with default or global configuration settings. .. versionchanged:: 0.4 @@ -596,7 +595,7 @@ def set_defaults(self): self._set_resource_defaults(resource, settings) def _set_resource_defaults(self, resource, settings): - """ Low-level method which sets default values for one resource. + """Low-level method which sets default values for one resource. .. versionchanged:: 1.1.0 Added 'mongo_query_whitelist'. @@ -692,7 +691,7 @@ def _set_resource_defaults(self, resource, settings): self._set_resource_datasource(resource, schema, settings) def _set_resource_datasource(self, resource, schema, settings): - """ Set the default values for the resource 'datasource' setting. + """Set the default values for the resource 'datasource' setting. .. versionadded:: 0.7 """ @@ -715,7 +714,7 @@ def _set_resource_datasource(self, resource, schema, settings): settings["item_lookup"] = False def _set_resource_projection(self, ds, schema, settings): - """ Set datasource projection for a resource + """Set datasource projection for a resource .. versionchanged:: 0.6.3 Fix: If datasource source is specified no fields are included by @@ -791,7 +790,7 @@ def _set_resource_projection(self, ds, schema, settings): ) def set_schema_defaults(self, schema, id_field): - """ When not provided, fills individual schema settings with default + """When not provided, fills individual schema settings with default or global configuration settings. :param schema: the resource schema to be initialized with default @@ -822,14 +821,14 @@ def set_schema_defaults(self, schema, id_field): @property def api_prefix(self): - """ Prefix to API endpoints. + """Prefix to API endpoints. .. versionadded:: 0.2 """ return api_prefix(self.config["URL_PREFIX"], self.config["API_VERSION"]) def _add_resource_url_rules(self, resource, settings): - """ Builds the API url map for one resource. Methods are enabled for + """Builds the API url map for one resource. Methods are enabled for each mapped endpoint, as configured in the settings. .. versionchanged:: 0.5 @@ -903,7 +902,7 @@ def _add_resource_url_rules(self, resource, settings): ) def _init_url_rules(self): - """ Builds the API url map. Methods are enabled for each mapped + """Builds the API url map. Methods are enabled for each mapped endpoint, as configured in the settings. .. versionchanged:: 0.4 @@ -950,7 +949,7 @@ def _init_url_rules(self): ) def register_resource(self, resource, settings): - """ Registers new resource to the domain. + """Registers new resource to the domain. Under the hood this validates given settings, updates default values and adds necessary URL routes (builds api url map). @@ -1008,7 +1007,7 @@ def register_resource(self, resource, settings): self.config["DOMAIN"]["MONGO_CONNECT"] = connect def register_error_handlers(self): - """ Register custom error handlers so we make sure that all errors + """Register custom error handlers so we make sure that all errors return a parseable body. .. versionchanged: 0.6.5 @@ -1022,7 +1021,7 @@ def register_error_handlers(self): self.register_error_handler(code, error_endpoint) def _init_oplog(self): - """ If enabled, configures the OPLOG endpoint. + """If enabled, configures the OPLOG endpoint. .. versionchanged:: 0.7 Add 'u' field to oplog audit schema. See #846. @@ -1075,8 +1074,7 @@ def _init_media_endpoint(self): ) def _init_schema_endpoint(self): - """Configures the schema endpoint if set in configuration. - """ + """Configures the schema endpoint if set in configuration.""" endpoint = self.config["SCHEMA_ENDPOINT"] if endpoint: @@ -1097,7 +1095,7 @@ def _init_schema_endpoint(self): ) def __call__(self, environ, start_response): - """ If HTTP_X_METHOD_OVERRIDE is included with the request and method + """If HTTP_X_METHOD_OVERRIDE is included with the request and method override is allowed, make sure the override method is returned to Eve as the request method, so normal routing and method validation can be performed. diff --git a/eve/io/base.py b/eve/io/base.py index 41e893345..ac7b287a4 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -19,7 +19,7 @@ class BaseJSONEncoder(json.JSONEncoder): - """ Proprietary JSONEconder subclass used by the json render function. + """Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. """ @@ -38,7 +38,7 @@ def default(self, obj): class ConnectionException(Exception): - """ Raised when DataLayer subclasses cannot find/activate to their + """Raised when DataLayer subclasses cannot find/activate to their database connection. :param driver_exception: the original exception raised by the source db @@ -59,7 +59,7 @@ def __str__(self): class DataLayer(object): - """ Base data layer class. Defines the interface that actual data-access + """Base data layer class. Defines the interface that actual data-access classes, being subclasses, must implement. Implemented as a Flask extension. @@ -99,7 +99,7 @@ class OriginalChangedError(Exception): json_encoder_class = BaseJSONEncoder def __init__(self, app): - """ Implements the Flask extension pattern. + """Implements the Flask extension pattern. .. versionchanged:: 0.2 Explicit initialize self.driver to None. @@ -112,13 +112,13 @@ def __init__(self, app): self.app = None def init_app(self, app): - """ This is where you want to initialize the db driver so it will be + """This is where you want to initialize the db driver so it will be alive through the whole instance lifespan. """ raise NotImplementedError def find(self, resource, req, sub_resource_lookup, perform_count=True): - """ Retrieves a set of documents (rows), matching the current request. + """Retrieves a set of documents (rows), matching the current request. Consumed when a request hits a collection/document endpoint (`/people/`). @@ -143,7 +143,7 @@ def find(self, resource, req, sub_resource_lookup, perform_count=True): raise NotImplementedError def aggregate(self, resource, pipeline, options): - """ Perform an aggregation on the resource datasource and returns + """Perform an aggregation on the resource datasource and returns the result. Only implent this if the underlying db engine supports aggregation operations. @@ -165,7 +165,7 @@ def find_one( force_auth_field_projection=False, **lookup ): - """ Retrieves a single document/record. Consumed when a request hits an + """Retrieves a single document/record. Consumed when a request hits an item endpoint (`/people/id/`). :param resource: resource being accessed. You should then use the @@ -197,7 +197,7 @@ def find_one( raise NotImplementedError def find_one_raw(self, resource, **lookup): - """ Retrieves a single, raw document. No projections or datasource + """Retrieves a single, raw document. No projections or datasource filters are being applied here. Just looking up the document using the same lookup. @@ -209,7 +209,7 @@ def find_one_raw(self, resource, **lookup): raise NotImplementedError def find_list_of_ids(self, resource, ids, client_projection=None): - """ Retrieves a list of documents based on a list of primary keys + """Retrieves a list of documents based on a list of primary keys The primary key is the field defined in `ID_FIELD`. This is a separate function to allow us to use per-database optimizations for this type of query. @@ -226,7 +226,7 @@ def find_list_of_ids(self, resource, ids, client_projection=None): raise NotImplementedError def insert(self, resource, doc_or_docs): - """ Inserts a document into a resource collection/table. + """Inserts a document into a resource collection/table. :param resource: resource being accessed. You should then use the ``datasource`` helper function to retrieve both @@ -241,7 +241,7 @@ def insert(self, resource, doc_or_docs): raise NotImplementedError def update(self, resource, id_, updates, original): - """ Updates a collection/table document/row. + """Updates a collection/table document/row. :param resource: resource being accessed. You should then use the ``datasource`` helper function to retrieve the actual datasource name. @@ -256,7 +256,7 @@ def update(self, resource, id_, updates, original): raise NotImplementedError def replace(self, resource, id_, document, original): - """ Replaces a collection/table document/row. + """Replaces a collection/table document/row. :param resource: resource being accessed. You should then use the ``datasource`` helper function to retrieve the actual datasource name. @@ -271,7 +271,7 @@ def replace(self, resource, id_, document, original): raise NotImplementedError def remove(self, resource, lookup): - """ Removes a document/row or an entire set of documents/rows from a + """Removes a document/row or an entire set of documents/rows from a database collection/table. :param resource: resource being accessed. You should then use @@ -288,7 +288,7 @@ def remove(self, resource, lookup): raise NotImplementedError def combine_queries(self, query_a, query_b): - """ Takes two db queries and applies db-specific syntax to produce + """Takes two db queries and applies db-specific syntax to produce the intersection. .. versionadded: 0.1.0 @@ -297,7 +297,7 @@ def combine_queries(self, query_a, query_b): raise NotImplementedError def get_value_from_query(self, query, field_name): - """ Parses the given potentially-complex query and returns the value + """Parses the given potentially-complex query and returns the value being assigned to the field given in `field_name`. This mainly exists to deal with more complicated compound queries @@ -308,7 +308,7 @@ def get_value_from_query(self, query, field_name): raise NotImplementedError def query_contains_field(self, query, field_name): - """ For the specified field name, does the query contain it? + """For the specified field name, does the query contain it? Used know whether we need to parse a compound query. .. versionadded: 0.1.0 @@ -317,7 +317,7 @@ def query_contains_field(self, query, field_name): raise NotImplementedError def is_empty(self, resource): - """ Returns True if the collection is empty; False otherwise. While + """Returns True if the collection is empty; False otherwise. While a user could rely on self.find() method to achieve the same result, this method can probably take advantage of specific datastore features to provide better performance. @@ -335,7 +335,7 @@ def is_empty(self, resource): raise NotImplementedError def datasource(self, resource): - """ Returns a tuple with the actual name of the database + """Returns a tuple with the actual name of the database collection/table, base query and projection for the resource being accessed. @@ -372,7 +372,7 @@ def _datasource_ex( check_auth_value=True, force_auth_field_projection=False, ): - """ Returns both db collection and exact query (base filter included) + """Returns both db collection and exact query (base filter included) to which an API resource refers to. .. versionchanged:: 0.5.2 @@ -511,7 +511,7 @@ def _datasource_ex( return datasource, query, fields, sort def _client_projection(self, req): - """ Returns a properly parsed client projection if available. + """Returns a properly parsed client projection if available. :param req: a :class:`ParsedRequest` instance. diff --git a/eve/io/media.py b/eve/io/media.py index 74d3ba79a..82d4436a1 100644 --- a/eve/io/media.py +++ b/eve/io/media.py @@ -12,7 +12,7 @@ class MediaStorage(object): - """ The MediaStorage class provides a standardized API for storing files, + """The MediaStorage class provides a standardized API for storing files, along with a set of default behaviors that all other storage systems can inherit or override as necessary. @@ -28,14 +28,14 @@ def __init__(self, app=None): self.app = app def get(self, id_or_filename, resource=None): - """ Opens the file given by name or unique id. Note that although the + """Opens the file given by name or unique id. Note that although the returned file is guaranteed to be a File object, it might actually be some subclass. Returns None if no file was found. """ raise NotImplementedError def put(self, content, filename=None, content_type=None, resource=None): - """ Saves a new file using the storage system, preferably with the name + """Saves a new file using the storage system, preferably with the name specified. If there already exists a file with this name name, the storage system may modify the filename as necessary to get a unique name. Depending on the storage system, a unique id or the actual name @@ -48,14 +48,14 @@ def put(self, content, filename=None, content_type=None, resource=None): raise NotImplementedError def delete(self, id_or_filename, resource=None): - """ Deletes the file referenced by name or unique id. If deletion is + """Deletes the file referenced by name or unique id. If deletion is not supported on the target storage system this will raise NotImplementedError instead """ raise NotImplementedError def exists(self, id_or_filename, resource=None): - """ Returns True if a file referenced by the given name or unique id + """Returns True if a file referenced by the given name or unique id already exists in the storage system, or False if the name is available for a new file. """ diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 898c11904..07883a0a0 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -17,7 +17,7 @@ class GridFSMediaStorage(MediaStorage): - """ The GridFSMediaStorage class stores files into GridFS. + """The GridFSMediaStorage class stores files into GridFS. ..versionadded:: 0.3 """ @@ -37,7 +37,7 @@ def __init__(self, app=None): self._fs = {} def validate(self): - """ Make sure that the application data layer is a eve.io.mongo.Mongo + """Make sure that the application data layer is a eve.io.mongo.Mongo instance. """ if self.app is None: @@ -47,7 +47,7 @@ def validate(self): raise TypeError("Application object must be a Eve application") def fs(self, resource=None): - """ Provides the instance-level GridFS instance, instantiating it if + """Provides the instance-level GridFS instance, instantiating it if needed. .. versionchanged:: 0.6 @@ -63,7 +63,7 @@ def fs(self, resource=None): return self._fs[px] def get(self, _id, resource=None): - """ Returns the file given by unique id. Returns None if no file was + """Returns the file given by unique id. Returns None if no file was found. .. versionchanged: 0.6 @@ -85,7 +85,7 @@ def get(self, _id, resource=None): return _file def put(self, content, filename=None, content_type=None, resource=None): - """ Saves a new file in GridFS. Returns the unique id of the stored + """Saves a new file in GridFS. Returns the unique id of the stored file. Also stores content type of the file. """ return self.fs(resource).put( @@ -93,12 +93,11 @@ def put(self, content, filename=None, content_type=None, resource=None): ) def delete(self, _id, resource=None): - """ Deletes the file referenced by unique id. - """ + """Deletes the file referenced by unique id.""" self.fs(resource).delete(_id) def exists(self, id_or_document, resource=None): - """ Returns True if a file referenced by the unique id or the query + """Returns True if a file referenced by the unique id or the query document already exists, False otherwise. Valid query: {'filename': 'file.txt'} diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index b84f4bc9b..41093a78c 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -40,7 +40,7 @@ class MongoJSONEncoder(BaseJSONEncoder): - """ Proprietary JSONEconder subclass used by the json render function. + """Proprietary JSONEconder subclass used by the json render function. This is needed to address the encoding of special values. .. versionchanged:: 0.8.2 @@ -75,7 +75,7 @@ def default(self, obj): class Mongo(DataLayer): - """ MongoDB data access layer for Eve REST API. + """MongoDB data access layer for Eve REST API. .. versionchanged:: 0.5 Properly serialize nullable float and integers. #469. @@ -130,7 +130,7 @@ class Mongo(DataLayer): ) def init_app(self, app): - """ Initialize PyMongo. + """Initialize PyMongo. .. versionchanged:: 0.6 Use mongo_prefix for multidb support. @@ -143,7 +143,7 @@ def init_app(self, app): self.mongo_prefix = None def find(self, resource, req, sub_resource_lookup, perform_count=True): - """ Retrieves a set of documents matching a given request. Queries can + """Retrieves a set of documents matching a given request. Queries can be expressed in two different formats: the mongo query syntax, and the python syntax. The first kind of query would look like: :: @@ -299,7 +299,7 @@ def find_one( force_auth_field_projection=False, **lookup ): - """ Retrieves a single document. + """Retrieves a single document. :param resource: resource name. :param req: a :class:`ParsedRequest` instance. @@ -350,7 +350,7 @@ def find_one( ) def find_one_raw(self, resource, **lookup): - """ Retrieves a single raw document. + """Retrieves a single raw document. :param resource: resource name. :param **lookup: lookup query. @@ -369,7 +369,7 @@ def find_one_raw(self, resource, **lookup): return self.pymongo(resource).db[datasource].find_one(lookup) def find_list_of_ids(self, resource, ids, client_projection=None): - """ Retrieves a list of documents from the collection given + """Retrieves a list of documents from the collection given by `resource`, matching the given list of ids. This query is generated to *preserve the order* of the elements @@ -429,7 +429,7 @@ def aggregate(self, resource, pipeline, options): return self.pymongo(resource).db[datasource].aggregate(challenge, **options) def insert(self, resource, doc_or_docs): - """ Inserts a document into a resource collection. + """Inserts a document into a resource collection. .. versionchanged:: 0.6.1 Support for PyMongo 3.0. @@ -491,7 +491,7 @@ def insert(self, resource, doc_or_docs): ) def _change_request(self, resource, id_, changes, original, replace=False): - """ Performs a change, be it a replace or update. + """Performs a change, be it a replace or update. .. versionchanged:: 0.8.2 Return 400 if update/replace with malformed DBRef field. See #1257. @@ -561,7 +561,7 @@ def _change_request(self, resource, id_, changes, original, replace=False): ) def update(self, resource, id_, updates, original): - """ Updates a collection document. + """Updates a collection document. .. versionchanged:: 0.6 Support for multiple databases. @@ -595,7 +595,7 @@ def update(self, resource, id_, updates, original): return self._change_request(resource, id_, {"$set": updates}, original) def replace(self, resource, id_, document, original): - """ Replaces an existing document. + """Replaces an existing document. .. versionchanged:: 0.6 Support for multiple databases. @@ -616,7 +616,7 @@ def replace(self, resource, id_, document, original): return self._change_request(resource, id_, document, original, replace=True) def remove(self, resource, lookup): - """ Removes a document or the entire set of documents from a + """Removes a document or the entire set of documents from a collection. .. versionchanged:: 0.6.1 @@ -671,7 +671,7 @@ def remove(self, resource, lookup): # of a separate MonqoQuery class def combine_queries(self, query_a, query_b): - """ Takes two db queries and applies db-specific syntax to produce + """Takes two db queries and applies db-specific syntax to produce the intersection. This is used because we can't just dump one set of query operators @@ -708,7 +708,7 @@ def combine_queries(self, query_a, query_b): } def get_value_from_query(self, query, field_name): - """ For the specified field name, parses the query and returns + """For the specified field name, parses the query and returns the value being assigned in the query. For example, @@ -734,7 +734,7 @@ def get_value_from_query(self, query, field_name): raise KeyError def query_contains_field(self, query, field_name): - """ For the specified field name, does the query contain it? + """For the specified field name, does the query contain it? Used know whether we need to parse a compound query. .. versionadded: 0.1.0 @@ -747,7 +747,7 @@ def query_contains_field(self, query, field_name): return True def is_empty(self, resource): - """ Returns True if resource is empty; False otherwise. If there is + """Returns True if resource is empty; False otherwise. If there is no predefined filter on the resource we're relying on the db.collection.count_documents. However, if we do have a predefined filter we have to fallback on the find() method, which can be much @@ -785,7 +785,7 @@ def is_empty(self, resource): ) def _mongotize(self, source, resource, parse_objectid=False): - """ Recursively iterates a JSON dictionary, turning RFC-1123 strings + """Recursively iterates a JSON dictionary, turning RFC-1123 strings into datetime values and ObjectId-link strings into ObjectIds. .. versionchanged:: 0.3 @@ -881,7 +881,7 @@ def dict_sub_schema(base): return source def _sanitize(self, resource, spec): - """ Makes sure that only allowed operators are included in the query, + """Makes sure that only allowed operators are included in the query, aborts with a 400 otherwise. .. versionchanged:: 1.1.0 @@ -934,7 +934,7 @@ def sanitize_keys(spec): return spec def _convert_sort_request_to_dict(self, req): - """ Converts the contents of a `ParsedRequest`'s `sort` property to + """Converts the contents of a `ParsedRequest`'s `sort` property to a dict """ client_sort = {} @@ -959,7 +959,7 @@ def _convert_sort_request_to_dict(self, req): return client_sort def _convert_where_request_to_dict(self, resource, req): - """ Converts the contents of a `ParsedRequest`'s `where` property to + """Converts the contents of a `ParsedRequest`'s `where` property to a dict """ query = {} @@ -983,14 +983,14 @@ def _convert_where_request_to_dict(self, resource, req): return query def _wc(self, resource): - """ Syntactic sugar for the current collection write_concern setting. + """Syntactic sugar for the current collection write_concern setting. .. versionadded:: 0.0.8 """ return config.DOMAIN[resource]["mongo_write_concern"] def current_mongo_prefix(self, resource=None): - """ Returns the active mongo_prefix that should be used to retrieve + """Returns the active mongo_prefix that should be used to retrieve a valid PyMongo instance from the cache. If 'self.mongo_prefix' is set it has precedence over both endpoint (resource) and default drivers. This allows Auth classes (for instance) to override default settings to @@ -1039,7 +1039,7 @@ def current_mongo_prefix(self, resource=None): return px def pymongo(self, resource=None, prefix=None): - """ Returns an active PyMongo instance. If 'prefix' is defined then + """Returns an active PyMongo instance. If 'prefix' is defined then it has precedence over the endpoint ('resource') and/or 'self.mongo_instance'. @@ -1064,7 +1064,7 @@ def pymongo(self, resource=None, prefix=None): raise ConnectionException(e) def get_collection_with_write_concern(self, datasource, resource): - """ Returns a pymongo Collection with the desired write_concern + """Returns a pymongo Collection with the desired write_concern setting. PyMongo 3.0+ collections are immutable, yet we still want to allow the @@ -1078,7 +1078,7 @@ def get_collection_with_write_concern(self, datasource, resource): class PyMongos(dict): - """ Cache for PyMongo instances. It is just a normal dict which exposes + """Cache for PyMongo instances. It is just a normal dict which exposes a 'db' property for backward compatibility. .. versionadded:: 0.6 @@ -1090,7 +1090,7 @@ def __init__(self, mongo, *args): @property def db(self): - """ Returns the 'default' PyMongo instance, which is either the + """Returns the 'default' PyMongo instance, which is either the 'Mongo.mongo_prefix' value or 'MONGO'. This property is useful for backward compatibility as many custom Auth classes use the now obsolete 'self.data.driver.db[collection]' pattern. @@ -1099,7 +1099,7 @@ def db(self): def ensure_mongo_indexes(app, resource): - """ Make sure 'mongo_indexes' is respected and mongo indexes are created on + """Make sure 'mongo_indexes' is respected and mongo indexes are created on the current database. .. versionaddded:: 0.8 @@ -1119,7 +1119,7 @@ def ensure_mongo_indexes(app, resource): def _create_index(app, resource, name, list_of_keys, index_options): - """ Create a specific index composed of the `list_of_keys` for the + """Create a specific index composed of the `list_of_keys` for the mongo collection behind the `resource` using the `app.config` to retrieve all data needed to find out the mongodb configuration. The index is also configured by the `index_options`. diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index af0d811ef..cbe6fd886 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -18,7 +18,7 @@ def parse(expression): - """ Given a python-like conditional statement, returns the equivalent + """Given a python-like conditional statement, returns the equivalent mongo-like query expression. Conditional and boolean operators (==, <=, >=, !=, >, <) along with a couple function calls (ObjectId(), datetime()) are supported. @@ -38,7 +38,7 @@ class ParseError(ValueError): class MongoVisitor(ast.NodeVisitor): - """ Implements the python-to-mongo parser. Only Python conditional + """Implements the python-to-mongo parser. Only Python conditional statements are supported, however nested, combined with most common compare and boolean operators (And and Or). @@ -58,8 +58,7 @@ class MongoVisitor(ast.NodeVisitor): } def visit_Module(self, node): - """ Module handler, our entry point. - """ + """Module handler, our entry point.""" self.mongo_query = {} self.ops = [] self.current_value = None @@ -77,8 +76,7 @@ def visit_Module(self, node): ) def visit_Expr(self, node): - """ Make sure that we are parsing compare or boolean operators - """ + """Make sure that we are parsing compare or boolean operators""" if not ( isinstance(node.value, ast.Compare) or isinstance(node.value, ast.BoolOp) ): @@ -86,8 +84,7 @@ def visit_Expr(self, node): self.generic_visit(node) def visit_Compare(self, node): - """ Compare operator handler. - """ + """Compare operator handler.""" self.visit(node.left) left = self.current_value @@ -108,8 +105,7 @@ def visit_Compare(self, node): self.mongo_query[left] = value def visit_BoolOp(self, node): - """ Boolean operator handler. - """ + """Boolean operator handler.""" op = self.op_mapper[node.op.__class__] self.ops.append([]) for value in node.values: @@ -122,7 +118,7 @@ def visit_BoolOp(self, node): self.mongo_query[op] = c def visit_Call(self, node): - """ A couple function calls are supported: bson's ObjectId() and + """A couple function calls are supported: bson's ObjectId() and datetime(). """ if isinstance(node.func, ast.Name): @@ -141,22 +137,18 @@ def visit_Call(self, node): pass def visit_Attribute(self, node): - """ Attribute handler ('Contact.Id'). - """ + """Attribute handler ('Contact.Id').""" self.visit(node.value) self.current_value += "." + node.attr def visit_Name(self, node): - """ Names handler. - """ + """Names handler.""" self.current_value = node.id def visit_Num(self, node): - """ Numbers handler. - """ + """Numbers handler.""" self.current_value = node.n def visit_Str(self, node): - """ Strings handler. - """ + """Strings handler.""" self.current_value = node.s diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 7abe7bf34..ad18ec1eb 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -34,7 +34,7 @@ class Validator(Validator): - """ A cerberus.Validator subclass adding the `unique` contraint to + """A cerberus.Validator subclass adding the `unique` contraint to Cerberus standard validation. :param schema: the validation schema, to be composed according to Cerberus @@ -86,7 +86,7 @@ def _validate_unique(self, unique, field, value): self._is_value_unique(unique, field, value, {}) def _is_value_unique(self, unique, field, value, query): - """ Validates that a field value is unique. + """Validates that a field value is unique. .. versionchanged:: 0.6.2 Exclude soft deleted documents from uniqueness check. Closes #831. @@ -139,13 +139,13 @@ def _is_value_unique(self, unique, field, value, query): self._error(field, "value '%s' is not unique" % value) def _validate_data_relation(self, data_relation, field, value): - """ {'type': 'dict', - 'schema': { - 'resource': {'type': 'string', 'required': True}, - 'field': {'type': 'string', 'required': True}, - 'embeddable': {'type': 'boolean', 'default': False}, - 'version': {'type': 'boolean', 'default': False} - }} """ + """{'type': 'dict', + 'schema': { + 'resource': {'type': 'string', 'required': True}, + 'field': {'type': 'string', 'required': True}, + 'embeddable': {'type': 'boolean', 'default': False}, + 'version': {'type': 'boolean', 'default': False} + }}""" if not value and self.schema[field].get("nullable"): return @@ -275,7 +275,7 @@ def _validate_type_geometrycollection(self, value): pass def _validate_type_feature(self, value): - """ Enables validation for `feature`data type + """Enables validation for `feature`data type :param value: field value """ @@ -286,7 +286,7 @@ def _validate_type_feature(self, value): pass def _validate_type_featurecollection(self, value): - """ Enables validation for `featurecollection`data type + """Enables validation for `featurecollection`data type :param value: field value """ diff --git a/eve/logging.py b/eve/logging.py index 609b1cc0d..47ec40025 100644 --- a/eve/logging.py +++ b/eve/logging.py @@ -10,7 +10,7 @@ class RequestFilter(logging.Filter): - """ Adds Flask's request metadata to the log record so handlers can log + """Adds Flask's request metadata to the log record so handlers can log this information too. import logging diff --git a/eve/methods/common.py b/eve/methods/common.py index 0d548fdb7..50dcc33f8 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -43,7 +43,7 @@ def get_document( mongo_options=None, **lookup ): - """ Retrieves and return a single document. Since this function is used by + """Retrieves and return a single document. Since this function is used by the editing methods (PUT, PATCH, DELETE), we make sure that the client request references the current representation of the document before returning it. However, this concurrency control may be turned off by @@ -129,7 +129,7 @@ def get_document( def parse(value, resource): - """ Safely evaluates a string containing a Python expression. We are + """Safely evaluates a string containing a Python expression. We are receiving json and returning a dict. :param value: the string to be evaluated. @@ -168,7 +168,7 @@ def parse(value, resource): def payload(): - """ Performs sanity checks or decoding depending on the Content-Type, + """Performs sanity checks or decoding depending on the Content-Type, then returns the request payload as a dict. If request Content-Type is unsupported, aborts with a 400 (Bad Request). @@ -233,7 +233,7 @@ def payload(): def multidict_to_dict(multidict): - """ Convert a MultiDict containing form data into a regular dict. If the + """Convert a MultiDict containing form data into a regular dict. If the config setting AUTO_COLLAPSE_MULTI_KEYS is True, multiple values with the same key get entered as a list. If it is False, the first entry is picked. """ @@ -248,7 +248,7 @@ def multidict_to_dict(multidict): class RateLimit(object): - """ Implements the Rate-Limiting logic using Redis as a backend. + """Implements the Rate-Limiting logic using Redis as a backend. :param key_prefix: the key used to uniquely identify a client. :param limit: requests limit, per period. @@ -277,7 +277,7 @@ def __init__(self, key, limit, period, send_x_headers=True): def get_rate_limit(): - """ If available, returns a RateLimit instance which is valid for the + """If available, returns a RateLimit instance which is valid for the current request-response. .. versionadded:: 0.0.7 @@ -286,7 +286,7 @@ def get_rate_limit(): def ratelimit(): - """ Enables support for Rate-Limits on API methods + """Enables support for Rate-Limits on API methods The key is constructed by default from the remote address or the authorization.username if authentication is being used. On a authentication-only API, this will impose a ratelimit even on @@ -330,7 +330,7 @@ def rate_limited(*args, **kwargs): def last_updated(document): - """ Fixes document's LAST_UPDATED field value. Flask-PyMongo returns + """Fixes document's LAST_UPDATED field value. Flask-PyMongo returns timezone-aware values while stdlib datetime values are timezone-naive. Comparisons between the two would fail. @@ -354,7 +354,7 @@ def last_updated(document): def date_created(document): - """ If DATE_CREATED is missing we assume that it has been created outside + """If DATE_CREATED is missing we assume that it has been created outside of the API context and inject a default value. By design all documents return a DATE_CREATED (and we dont' want to break existing clients). @@ -370,7 +370,7 @@ def date_created(document): def epoch(): - """ A datetime.min alternative which won't crash on us. + """A datetime.min alternative which won't crash on us. .. versionchanged:: 0.1.0 Moved to common.py and renamed as public, so it can also be used by edit @@ -382,7 +382,7 @@ def epoch(): def serialize(document, resource=None, schema=None, fields=None): - """ Recursively handles field values that require data-aware serialization. + """Recursively handles field values that require data-aware serialization. Relies on the app.data.serializers dictionary. .. versionchanged: 0.8.1 @@ -558,7 +558,7 @@ def serialize_value(field_type, value): def normalize_dotted_fields(document): - """ Normalizes eventual dotted fields so validation can be performed + """Normalizes eventual dotted fields so validation can be performed seamlessly. For example this document: {"location.city": "a nested city"} @@ -603,7 +603,7 @@ def normalize_dotted_fields(document): def build_response_document(document, resource, embedded_fields, latest_doc=None): - """ Prepares a document for response including generation of ETag and + """Prepares a document for response including generation of ETag and metadata fields. :param document: the document to embed other documents into. @@ -674,7 +674,7 @@ def build_response_document(document, resource, embedded_fields, latest_doc=None def resolve_resource_projection(document, resource): - """ Purges a document of fields that are not included in its resource + """Purges a document of fields that are not included in its resource projecton. :param document: the original document. @@ -702,7 +702,7 @@ def resolve_resource_projection(document, resource): def field_definition(resource, chained_fields): - """ Resolves query string to resource with dot notation like + """Resolves query string to resource with dot notation like 'people.address.city' and returns corresponding field definition of the resource @@ -738,7 +738,7 @@ def field_definition(resource, chained_fields): def resolve_data_relation_links(document, resource): - """ Resolves all fields in a document that has data relation to other resources + """Resolves all fields in a document that has data relation to other resources :param document: the document to include data relation links. :param resource: the resource name. @@ -802,7 +802,7 @@ def resolve_data_relation_links(document, resource): def resolve_embedded_fields(resource, req): - """ Returns a list of validated embedded fields from the incoming request + """Returns a list of validated embedded fields from the incoming request or from the resource definition is the request does not specify. :param resource: the resource name. @@ -855,14 +855,14 @@ def resolve_embedded_fields(resource, req): def embedded_document(references, data_relation, field_name): - """ Returns a document to be embedded by reference using data_relation - taking into account document versions + """Returns a document to be embedded by reference using data_relation + taking into account document versions - :param reference: reference to the document to be embedded. - :param data_relation: the relation schema definition. - :param field_name: field name used in abort message only + :param reference: reference to the document to be embedded. + :param data_relation: the relation schema definition. + :param field_name: field name used in abort message only -) .. versionadded:: 0.5 + ) .. versionadded:: 0.5 """ embedded_docs = [] @@ -939,13 +939,13 @@ def embedded_document(references, data_relation, field_name): def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): - """ Sorts the documents fetched from the database + """Sorts the documents fetched from the database - :param embedded_docs: the documents fetch from the database. - :param id_value_to_sort: id_value sort criteria. - :param list_of_id_field_name: list of name of fields - :return embedded_docs: the list of documents sorted as per input - """ + :param embedded_docs: the documents fetch from the database. + :param id_value_to_sort: id_value sort criteria. + :param list_of_id_field_name: list of name of fields + :return embedded_docs: the list of documents sorted as per input + """ id_field_name_occurrences = Counter(list_of_id_field_name) temp_embedded_docs = [] @@ -968,14 +968,14 @@ def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): def sort_per_resource(embedded_docs, id_values_to_sort, id_field_name): - """ Sorts the documents fetched from the database per single resource - - :param embedded_docs: list of the documents fetched from the database. - :param id_values_to_sort: list of the id_values sort criteria. - :param list_of_id_field_name: list of name of fields - :param id_field_name: key name of the id field; `_id` - :return embedded_docs: the list of documents sorted as per input - """ + """Sorts the documents fetched from the database per single resource + + :param embedded_docs: list of the documents fetched from the database. + :param id_values_to_sort: list of the id_values sort criteria. + :param list_of_id_field_name: list of name of fields + :param id_field_name: key name of the id field; `_id` + :return embedded_docs: the list of documents sorted as per input + """ if id_values_to_sort is None: id_values_to_sort = [] embedded_docs = [x for x in embedded_docs if x is not None] @@ -989,17 +989,17 @@ def sort_per_resource(embedded_docs, id_values_to_sort, id_field_name): def generate_query_and_sorting_criteria(data_relation, references): - """ Generate query and sorting critiria - - :param data_relation: data relation for the resource. - :param references: DBRef or id to use to embed the document. - :returns id_value_to_sort: list of ids to use in the sort - list_of_id_field_name: list of field name (important only for - DBRef) - subresources_query: the list of query to perform per resource - (in case is not DBRef, it will be only one - query) - """ + """Generate query and sorting critiria + + :param data_relation: data relation for the resource. + :param references: DBRef or id to use to embed the document. + :returns id_value_to_sort: list of ids to use in the sort + list_of_id_field_name: list of field name (important only for + DBRef) + subresources_query: the list of query to perform per resource + (in case is not DBRef, it will be only one + query) + """ query = {"$or": []} subresources_query = {} old_subresource = "" @@ -1041,7 +1041,7 @@ def add_query_to_list(query, subresource, subresource_query): def subdocuments(fields_chain, resource, document): - """ Traverses the given document and yields subdocuments which + """Traverses the given document and yields subdocuments which correspond to the given fields_chain :param fields_chain: list of nested field names. @@ -1070,7 +1070,7 @@ def subdocuments(fields_chain, resource, document): def resolve_embedded_documents(document, resource, embedded_fields): - """ Loops through the documents, adding embedded representations + """Loops through the documents, adding embedded representations of any fields that are (1) defined eligible for embedding in the DOMAIN and (2) requested to be embedded in the current `req`. @@ -1114,7 +1114,7 @@ def resolve_embedded_documents(document, resource, embedded_fields): def resolve_media_files(document, resource): - """ Embed media files into the response document. + """Embed media files into the response document. :param document: the document eventually containing the media files. :param resource: the resource being consumed by the request. @@ -1175,7 +1175,7 @@ def resolve_one_media(file_id, resource): def marshal_write_response(document, resource): - """ Limit response document to minimize bandwidth when client supports it. + """Limit response document to minimize bandwidth when client supports it. :param document: the response document. :param resource: the resource being consumed by the request. @@ -1205,7 +1205,7 @@ def marshal_write_response(document, resource): def store_media_files(document, resource, original=None): - """ Store any media file in the underlying media store and update the + """Store any media file in the underlying media store and update the document with unique ids of stored files. :param document: the document eventually containing the media files. @@ -1256,7 +1256,7 @@ def store_media_files(document, resource, original=None): def resource_media_fields(document, resource): - """ Returns a list of media fields defined in the resource schema. + """Returns a list of media fields defined in the resource schema. :param document: the document eventually containing the media files. :param resource: the resource being consumed by the request. @@ -1284,7 +1284,7 @@ def resolve_sub_resource_path(document, resource): def resolve_user_restricted_access(document, resource): - """ Adds user restricted access metadata to the document if applicable. + """Adds user restricted access metadata to the document if applicable. :param document: the document being posted or replaced :param resource: the resource to which the document belongs @@ -1309,7 +1309,7 @@ def resolve_user_restricted_access(document, resource): def resolve_document_etag(documents, resource): - """ Adds etags to documents. + """Adds etags to documents. .. versionadded:: 0.5 """ @@ -1324,7 +1324,7 @@ def resolve_document_etag(documents, resource): def pre_event(f): - """ Enable a Hook pre http request. + """Enable a Hook pre http request. .. versionchanged:: 0.6 Enable callback hooks for HEAD requests. @@ -1373,7 +1373,7 @@ def decorated(*args, **kwargs): def document_link(resource, document_id, version=None): - """ Returns a link to a document endpoint. + """Returns a link to a document endpoint. :param resource: the resource name. :param document_id: the document unique identifier. @@ -1402,7 +1402,7 @@ def document_link(resource, document_id, version=None): def resource_link(resource=None): - """ Returns the current resource path relative to the API entry point. + """Returns the current resource path relative to the API entry point. Mostly going to be used by hateoas functions when building document/resource links. The resource URL stored in the config settings might contain regexes and custom variable names, all of which are not @@ -1441,7 +1441,7 @@ def strip_prefix(hit): def oplog_push(resource, document, op, id=None): - """ Pushes an edit operation to the oplog if included in OPLOG_METHODS. To + """Pushes an edit operation to the oplog if included in OPLOG_METHODS. To save on storage space (at least on MongoDB) field names are shortened: 'r' = resource endpoint, diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 36bfd5819..c740adc37 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -53,7 +53,7 @@ def deleteitem(resource, **lookup): def deleteitem_internal( resource, concurrency_check=False, suppress_callbacks=False, original=None, **lookup ): - """ Intended for internal delete calls, this method is not rate limited, + """Intended for internal delete calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Deletes a resource item. @@ -192,7 +192,7 @@ def deleteitem_internal( @requires_auth("resource") @pre_event def delete(resource, **lookup): - """ Deletes all item of a resource (collection in MongoDB terms). Won't + """Deletes all item of a resource (collection in MongoDB terms). Won't drop indexes. Use with caution! .. versionchanged:: 0.5 diff --git a/eve/methods/get.py b/eve/methods/get.py index 9f08be17a..04f9df61e 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -52,7 +52,7 @@ def get(resource, **lookup): def get_internal(resource, **lookup): - """ Retrieves the resource documents that match the current request. + """Retrieves the resource documents that match the current request. :param resource: the name of the resource. @@ -547,7 +547,7 @@ def getitem_internal(resource, **lookup): def _pagination_links(resource, req, document_count, document_id=None): - """ Returns the appropriate set of resource links depending on the + """Returns the appropriate set of resource links depending on the current page and the total number of documents returned by the query. :param resource: the resource name. @@ -670,7 +670,7 @@ def _pagination_links(resource, req, document_count, document_id=None): def _other_params(args): - """ Returns a multidict of params that are not used internally by Eve. + """Returns a multidict of params that are not used internally by Eve. :param args: multidict containing the request parameters """ @@ -692,7 +692,7 @@ def _other_params(args): def _meta_links(req, count): - """ Reterns the meta links for a paginated query. + """Reterns the meta links for a paginated query. :param req: parsed request object. :param count: total number of documents in a query. diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 3ee420775..891447ac4 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -55,9 +55,14 @@ def patch(resource, payload=None, **lookup): def patch_internal( - resource, payload=None, concurrency_check=False, skip_validation=False, mongo_options=None, **lookup + resource, + payload=None, + concurrency_check=False, + skip_validation=False, + mongo_options=None, + **lookup ): - """ Intended for internal patch calls, this method is not rate limited, + """Intended for internal patch calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Performs a document patch/update. Updates are first validated against the resource schema. If validation @@ -273,7 +278,7 @@ def patch_internal( def resolve_nested_documents(updates, original): - """ Nested document updates are merged with the original contents + """Nested document updates are merged with the original contents we don't overwrite the whole thing. See #519 for details. .. versionadded:: 0.5 diff --git a/eve/methods/put.py b/eve/methods/put.py index b2dcd44d4..1cf871a0b 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -60,7 +60,7 @@ def put(resource, payload=None, **lookup): def put_internal( resource, payload=None, concurrency_check=False, skip_validation=False, **lookup ): - """ Intended for internal put calls, this method is not rate limited, + """Intended for internal put calls, this method is not rate limited, authentication is not checked, pre-request events are not raised, and concurrency checking is optional. Performs a document replacement. Updates are first validated against the resource schema. If validation diff --git a/eve/render.py b/eve/render.py index bc539eb7c..f53839283 100644 --- a/eve/render.py +++ b/eve/render.py @@ -29,7 +29,7 @@ def raise_event(f): - """ Raises both general and resource-level events after the decorated + """Raises both general and resource-level events after the decorated function has been executed. Returns both the flask.request object and the response payload to the callback. @@ -66,7 +66,7 @@ def decorated(*args, **kwargs): @raise_event def send_response(resource, response): - """ Prepares the response for the client. + """Prepares the response for the client. :param resource: the resource involved. :param response: either a flask.Response object or a tuple. The former will @@ -95,7 +95,7 @@ def send_response(resource, response): def _prepare_response( resource, dct, last_modified=None, etag=None, status=200, headers=None ): - """ Prepares the response object according to the client request and + """Prepares the response object according to the client request and available renderers, making sure that all accessory directives (caching, etag, last-modified) are present. @@ -256,7 +256,7 @@ def _prepare_response( def _best_mime(): - """ Returns the best match between the requested mime type and the + """Returns the best match between the requested mime type and the ones supported by Eve. Along with the mime, also the corresponding render function is returns. @@ -288,7 +288,7 @@ def _best_mime(): class Renderer(object): - """ Base class for all the renderers. Renderer should set valid `mime` + """Base class for all the renderers. Renderer should set valid `mime` attr and have `.render()` method implemented. """ @@ -300,14 +300,12 @@ def render(self, data): class JSONRenderer(Renderer): - """ JSON renderer class based on `simplejson` package. - - """ + """JSON renderer class based on `simplejson` package.""" mime = ("application/json",) def render(self, data): - """ JSON render function + """JSON render function :param data: the data stream to be rendered as json. @@ -332,15 +330,13 @@ def render(self, data): class XMLRenderer(Renderer): - """ XML renderer class. - - """ + """XML renderer class.""" mime = ("application/xml", "text/xml", "application/x-xml") tag = "XML" def render(self, data): - """ XML render function. + """XML render function. :param data: the data stream to be rendered as xml. @@ -370,7 +366,7 @@ def render(self, data): @classmethod def xml_root_open(cls, data): - """ Returns the opening tag for the XML root node. If the datastream + """Returns the opening tag for the XML root node. If the datastream includes information about resource endpoints (href, title), they will be added as node attributes. The resource endpoint is then removed to allow for further processing of the datastream. @@ -396,7 +392,7 @@ def xml_root_open(cls, data): @classmethod def xml_add_meta(cls, data): - """ Returns a meta node with page, total, max_results fields. + """Returns a meta node with page, total, max_results fields. :param data: the data stream to be rendered as xml. @@ -417,7 +413,7 @@ def xml_add_meta(cls, data): @classmethod def xml_add_links(cls, data): - """ Returns as many nodes as there are in the datastream. The + """Returns as many nodes as there are in the datastream. The added links are then removed from the datastream to allow for further processing. @@ -446,9 +442,9 @@ def xml_add_links(cls, data): data.update({config.LINKS: {rel: link}}) elif isinstance(link, list): - xml += "".join( + xml += "".join( chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) - for d in link + for d in link ) else: xml += "".join(chunk % (rel, utils.escape(link["href"]), link["title"])) @@ -456,7 +452,7 @@ def xml_add_links(cls, data): @classmethod def xml_add_items(cls, data): - """ When this function is called the datastream can only contain + """When this function is called the datastream can only contain a `_items` list, or a dictionary. If a list, each item is a resource which rendered as XML. If a dictionary, it will be rendered as XML. @@ -472,7 +468,7 @@ def xml_add_items(cls, data): @classmethod def xml_item(cls, item): - """ Represents a single resource (member of a collection) as XML. + """Represents a single resource (member of a collection) as XML. :param data: the data stream to be rendered as xml. @@ -486,7 +482,7 @@ def xml_item(cls, item): @classmethod def xml_root_close(cls): - """ Returns the closing tag of the XML root node. + """Returns the closing tag of the XML root node. .. versionadded:: 0.0.3 """ @@ -494,7 +490,7 @@ def xml_root_close(cls): @classmethod def xml_dict(cls, data): - """ Renders a dict as XML. + """Renders a dict as XML. :param data: the data stream to be rendered as xml. @@ -534,7 +530,7 @@ def xml_dict(cls, data): @classmethod def xml_field_open(cls, field, idx, related_links): - """ Returns opening tag for XML field element node. + """Returns opening tag for XML field element node. :param field: field name for the element node :param idx: the index in the data relation links if serializing a list of same field to XML @@ -560,7 +556,7 @@ def xml_field_open(cls, field, idx, related_links): @classmethod def xml_field_close(cls, field): - """ Returns closing tag of XML field element node. + """Returns closing tag of XML field element node. :param field: field name for the element node diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 33019ba48..3d5a024cb 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -65,7 +65,7 @@ def close_pymongo_connection(app): class TestMinimal(unittest.TestCase): - """ Start the building of the tests for an application + """Start the building of the tests for an application based on Eve by subclassing this class and provide proper settings using :func:`setUp()` """ @@ -73,7 +73,7 @@ class TestMinimal(unittest.TestCase): app = ValueStack(close_pymongo_connection) def setUp(self, settings_file=None, url_converters=None): - """ Prepare the test fixture + """Prepare the test fixture :param settings_file: the name of the settings file. Defaults to `eve/tests/test_settings.py`. diff --git a/eve/tests/auth.py b/eve/tests/auth.py index d5c4c8917..fbd3ae76c 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -489,7 +489,7 @@ def test_get(self): self.assertEqual(len(data2["_items"]), 1) def test_get_by_auth_field_criteria(self): - """ If we attempt to retrieve an object by the same field + """If we attempt to retrieve an object by the same field that is in `auth_field`, then the request is /unauthorized/, and should fail and return 401. @@ -502,8 +502,7 @@ def test_get_by_auth_field_criteria(self): self.assert401(status) def test_get_by_auth_field_id(self): - """ To test handling of ObjectIds - """ + """To test handling of ObjectIds""" # set auth_field to `_id` self.domain["users"][self.field_name] = self.domain["users"]["id_field"] @@ -513,7 +512,7 @@ def test_get_by_auth_field_id(self): self.assert401(status) def test_filter_by_auth_field_id(self): - """ To test handling of ObjectIds when using a `where` clause + """To test handling of ObjectIds when using a `where` clause We need to make sure we *match* an object ID when it is the same """ @@ -554,7 +553,7 @@ def test_filter_by_auth_field_id(self): self.assertEqual(len(data2["_items"]), 1) def test_collection_get_public(self): - """ Test that if GET is in `public_methods` the `auth_field` + """Test that if GET is in `public_methods` the `auth_field` criteria is overruled """ self.resource["public_methods"].append("GET") @@ -565,7 +564,7 @@ def test_collection_get_public(self): self.assertEqual(len(data["_items"]), 25) def test_item_get_public(self): - """ Test that if GET is in `public_item_methods` the `auth_field` + """Test that if GET is in `public_item_methods` the `auth_field` criteria is overruled """ self.resource["public_item_methods"].append("GET") @@ -646,7 +645,7 @@ def test_post_resource_auth(self): self.assertEqual(data["_items"][0]["ref"], json.loads(self.data)["ref"]) def test_post_bandwidth_saver_off_resource_auth(self): - """ Test that when BANDWIDTH_SAVER is turned off the auth_field is + """Test that when BANDWIDTH_SAVER is turned off the auth_field is not exposed in the response payload """ self.app.config["BANDWIDTH_SAVER"] = False @@ -786,7 +785,7 @@ def test_put_resource_auth(self): self.assertEqual(data["ref"], new_ref) def test_put_bandwidth_saver_off_resource_auth(self): - """ Test that when BANDWIDTH_SAVER is turned off the auth_field is + """Test that when BANDWIDTH_SAVER is turned off the auth_field is not exposed in the response payload """ self.app.config["BANDWIDTH_SAVER"] = False diff --git a/eve/tests/config.py b/eve/tests/config.py index cc7f9cd00..6b97659c3 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -519,7 +519,7 @@ def test_create_indexes(self): self.assertEqual(args[arg], indexes[key][arg]) def test_custom_error_handlers(self): - """ Test that the standard, custom error handler is registered for + """Test that the standard, custom error handler is registered for supported error codes. """ codes = self.app.config["STANDARD_ERRORS"] diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 03886deca..59516f4b1 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -14,7 +14,7 @@ class UUIDEncoder(BaseJSONEncoder): - """ Propretary JSONEconder subclass used by the json render function. + """Propretary JSONEconder subclass used by the json render function. This is different from BaseJSONEoncoder since it also addresses encoding of UUID """ @@ -250,7 +250,7 @@ def test_api_prefix_version(self): self.assert200(r.status_code) def test_api_prefix_version_hateoas_links(self): - """ Test that #419 is closed and URL_PREFIX and API_VERSION are stipped + """Test that #419 is closed and URL_PREFIX and API_VERSION are stipped out of hateoas links since they are now relative to the API entry point (root). """ diff --git a/eve/tests/io/media.py b/eve/tests/io/media.py index 62b0ccddb..6c9e5aa8f 100644 --- a/eve/tests/io/media.py +++ b/eve/tests/io/media.py @@ -274,7 +274,7 @@ def test_gridfs_media_storage_delete(self): self.assert404(s) def test_get_media_can_leverage_projection(self): - """ Test that static projection expose fields other than media + """Test that static projection expose fields other than media and client projection on media will work. """ # post a document with *hiding media* @@ -319,7 +319,7 @@ def test_get_media_can_leverage_projection(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_gridfs_media_storage_delete_projection(self): - """ test that #284 is fixed: If you have a media field, and set + """test that #284 is fixed: If you have a media field, and set datasource projection to 0 for that field, the media will not be deleted """ diff --git a/eve/tests/io/mongo.py b/eve/tests/io/mongo.py index e148e8918..3ac51c2f4 100644 --- a/eve/tests/io/mongo.py +++ b/eve/tests/io/mongo.py @@ -83,13 +83,13 @@ def test_bad_Expr(self): class TestMongoValidator(TestCase): def test_unique_fail(self): - """ relying on POST and PATCH tests since we don't have an active - app_context running here """ + """relying on POST and PATCH tests since we don't have an active + app_context running here""" pass def test_unique_success(self): - """ relying on POST and PATCH tests since we don't have an active - app_context running here """ + """relying on POST and PATCH tests since we don't have an active + app_context running here""" pass def test_decimal_fail(self): diff --git a/eve/tests/logging.py b/eve/tests/logging.py index 31cdf5c3f..8ca76a48c 100644 --- a/eve/tests/logging.py +++ b/eve/tests/logging.py @@ -3,7 +3,7 @@ class TestUtils(TestBase): - """ collection, document and home_link methods (and resource_uri, which is + """collection, document and home_link methods (and resource_uri, which is used by all of them) are tested in 'tests.methods' since we need an active flaskapp context """ diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 4adc5dfd8..7164caa97 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -604,8 +604,8 @@ def test_post_oplog(self): self.assertTrue("extra" not in oplog_entry) def test_post_oplog_does_not_alter_document(self): - """ Make sure we don't alter document ETag when performing an - oplog_push. See #590 and #1206. """ + """Make sure we don't alter document ETag when performing an + oplog_push. See #590 and #1206.""" self.app.config["OPLOG_CHANGE_METHODS"].append("POST") r = self.test_client.post( self.different_resource_url, @@ -649,8 +649,8 @@ def test_put_oplog(self): self.assertOpLogEntry(oplog_entry, "PUT") def test_put_oplog_does_not_alter_document(self): - """ Make sure we don't alter document ETag when performing an - oplog_push. See #590. """ + """Make sure we don't alter document ETag when performing an + oplog_push. See #590.""" self.headers.append(("If-Match", self.item_etag)) r = self.test_client.put( self.item_id_url, diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 721056b12..46cddc2c1 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -290,8 +290,7 @@ def test_delete(self): self.assertTrue(self.app.config["ERROR"] in data) def test_deleteitem_internal(self): - """Deleteitem internal should honor soft delete settings. - """ + """Deleteitem internal should honor soft delete settings.""" # test that deleteitem_internal is available and working properly. with self.app.test_request_context(self.item_id_url): r, _, _, status = deleteitem_internal( @@ -500,8 +499,7 @@ def test_softdeleted_get_response_skips_embedded_expansion(self): self.assertEqual(data["person"], str(fake_contact_id)) def test_softdelete_caching(self): - """404 Not Found responses after soft delete should be cacheable - """ + """404 Not Found responses after soft delete should be cacheable""" # Soft delete item r, status = self.delete(self.item_id_url, headers=self.etag_headers) self.assert204(status) @@ -626,7 +624,7 @@ def test_softdelete_db_fields(self): self.assertTrue(self.deleted_field in db_stored_doc) def test_exclusive_projection(self): - """ Test that when an exclusive projection is used in the 'datasource' + """Test that when an exclusive projection is used in the 'datasource' setting for the resource, enabling soft_deletes does not cause a 500 error. See #752. """ @@ -635,7 +633,7 @@ def test_exclusive_projection(self): self.assert200(status) def test_exclude_soft_deleted_documents_from_unique_checks(self): - """ Test that soft deleted documents are ignored when validating new + """Test that soft deleted documents are ignored when validating new documents against the 'unique' rule. See #831. """ unique_value = "1234567890123456789054321" @@ -683,7 +681,7 @@ def setUp(self): self.etag_headers = [("If-Match", self.item_etag)] def test_resource_specific_softdelete(self): - """ Resource level soft delete configuration should override + """Resource level soft delete configuration should override application configuration. """ # Confirm soft delete is enabled for known resource. diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 45640c995..09ce7c45a 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -140,7 +140,7 @@ def test_get_custom_page(self): self.assertPagination(response, 2, 101, 25) def test_get_pagination_no_documents(self): - """ test that pagination meta is present even when no records are being + """test that pagination meta is present even when no records are being returned. #415. """ response, status = self.get(self.known_resource, '?where={"ref": "not_really"}') @@ -305,8 +305,7 @@ def test_get_where_python_syntax1(self): self.assertEqual(len(resource), 1) def test_get_query_in_links(self): - """ Make sure that query strings appear in all HATEOAS links (#464). - """ + """Make sure that query strings appear in all HATEOAS links (#464).""" # find a role with enough results for role in ("agent", "client", "vendor"): where = "role == %s" % role @@ -330,8 +329,8 @@ def test_get_query_in_links(self): self.assertPrevLink(links, 1) def test_get_projection_consistent_etag(self): - """ Test that #369 is fixed and projection queries return consistent - etags (as they are now stored along with the document). + """Test that #369 is fixed and projection queries return consistent + etags (as they are now stored along with the document). """ etag_field = self.app.config["ETAG"] data = {"inv_number": self.random_string(10)} @@ -404,7 +403,7 @@ def test_get_static_projection(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_include_projection_can_exclude(self): - """ Test that static projection only expose fields included + """Test that static projection only expose fields included and support client projection on these fields. """ # exclude `ref` by client side @@ -433,7 +432,7 @@ def test_get_server_include_projection_can_exclude(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_include_projection_block_sniff(self): - """ Test that static projection only expose fields included + """Test that static projection only expose fields included and client projection on other fields will fail. """ # shouldn't work when including `prog` (excluded) by client side @@ -459,7 +458,7 @@ def test_get_server_include_projection_block_sniff(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_exclude_projection_can_project_others(self): - """ Test that static projection expose fields other than excluded + """Test that static projection expose fields other than excluded and support client projection on exposed fields. """ projection = '{"prog": 1, "location":1}' @@ -486,7 +485,7 @@ def test_get_server_exclude_projection_can_project_others(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_server_exlcude_projection_can_sniff(self): - """ Test that static projection expose fields other than excluded + """Test that static projection expose fields other than excluded and client projection on excluded **will work**. """ projection = '{"born": 1}' @@ -636,7 +635,7 @@ def test_get(self): self.assertGet(response, status) def test_get_same_collection_different_resource(self): - """ the 'users' resource is actually using the same db collection as + """the 'users' resource is actually using the same db collection as 'contacts'. Let's verify that base filters are being applied, and the right amount of items/links and the correct titles etc. are being returned. Of course 'contacts' itself has its own base filter, which @@ -759,8 +758,8 @@ def test_get_custom_auto_document_fields(self): self.assertTrue("_the_etag" in document) def test_get_embedded_media_validate_rest_of_fields(self): - """ test multipart/form-data resource fields that are JSON - encoded are validated correctly. #806 + """test multipart/form-data resource fields that are JSON + encoded are validated correctly. #806 """ self.app.config["MULTIPART_FORM_FIELDS_AS_JSON"] = True @@ -819,8 +818,7 @@ def test_get_embedded_media_validate_rest_of_fields(self): self.app.config["MULTIPART_FORM_FIELDS_AS_JSON"] = False def test_get_embedded_media(self): - """ test that embeedded images are properly rendered and #305 is fixed. - """ + """test that embeedded images are properly rendered and #305 is fixed.""" # add a 'digital_assets' endpoint to the API self.app.register_resource( @@ -1212,8 +1210,8 @@ def test_get_invalid_sort_syntax(self): self.assert400(status) def test_get_allowed_filters_operators(self): - """ test that supported operators are not considered invalid filters - (#388). Also, test that nested filters are validated. + """test that supported operators are not considered invalid filters + (#388). Also, test that nested filters are validated. """ where = '?where={"$and": [{"field1": "value1"}, {"field2": "value2"}]}' settings = self.app.config["DOMAIN"][self.known_resource] @@ -1229,8 +1227,7 @@ def test_get_allowed_filters_operators(self): self.assert400(status) def test_get_nested_filter_operators_unvalidated(self): - """ test that nested filter operators are working correctly. - """ + """test that nested filter operators are working correctly.""" where = "".join( ( '?where={"$and":[{"$or":[{"fldA":"valA"},', @@ -1241,8 +1238,7 @@ def test_get_nested_filter_operators_unvalidated(self): self.assert200(status) def test_get_nested_filter_operators_validated(self): - """ test that nested filter operators are working correctly. - """ + """test that nested filter operators are working correctly.""" self.app.config["VALIDATE_FILTERS"] = True where = "".join( @@ -1264,8 +1260,8 @@ def test_get_nested_filter_operators_validated(self): self.assert200(status) def test_get_invalid_where_fields(self): - """ test that checks all fields of the where clause to be valid - resource fields according to the resource schema. + """test that checks all fields of the where clause to be valid + resource fields according to the resource schema. """ self.app.config["VALIDATE_FILTERS"] = True diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index a2650d212..d72741378 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -220,10 +220,10 @@ def test_patch_null_objectid(self): self.assertEqual(db_value, test_value) def test_patch_missing_default(self): - """ PATCH an object which is missing a field with a default value. + """PATCH an object which is missing a field with a default value. This should result in setting the field to its default value, even if - the field is not provided in the PATCH's payload. """ + the field is not provided in the PATCH's payload.""" field = "ref" test_value = "1234567890123456789012345" changes = {field: test_value} @@ -236,10 +236,10 @@ def test_patch_missing_default(self): ) def test_patch_missing_default_with_post_override(self): - """ PATCH an object which is missing a field with a default value. + """PATCH an object which is missing a field with a default value. This should result in setting the field to its default value, even if - the field is not provided in the PATCH's payload. """ + the field is not provided in the PATCH's payload.""" field = "ref" test_value = "1234567890123456789012345" r = self.perform_patch_with_post_override(field, test_value) @@ -255,10 +255,10 @@ def test_patch_missing_default_with_post_override(self): ) def test_patch_missing_nested_default(self): - """ PATCH an object which is missing a field with a default value. + """PATCH an object which is missing a field with a default value. This should result in setting the field to its default value, even if - the field is not provided in the PATCH's payload. """ + the field is not provided in the PATCH's payload.""" field = "dict_with_nested_default" test_value = {} changes = {field: test_value} @@ -601,7 +601,7 @@ def test_patch_readonly_field_with_previous_document(self): self.assertTrue("is read-only" in r["_issues"]["read_only_field"]) def test_patch_nested_document_not_overwritten(self): - """ Test that nested documents are not overwritten on PATCH and #519 + """Test that nested documents are not overwritten on PATCH and #519 is fixed. """ @@ -660,7 +660,7 @@ def test_patch_nested_document_not_overwritten(self): self.assertEqual(int, 99) def test_patch_nested_document_no_merge(self): - """ Test that nested documents are not merged, but overwritten, + """Test that nested documents are not merged, but overwritten, if configured.""" domain = { "merge_nested_documents": False, @@ -718,7 +718,7 @@ def test_patch_nested_document_nullable_missing(self): self.assertEqual(r["other"], {"name": "other_name"}) def test_patch_dependent_field_on_origin_document(self): - """ Test that when patching a field which is dependent on another field's + """Test that when patching a field which is dependent on another field's existance, and this other field is not provided in the patch, but does exist on the persisted document, the patch will be accepted. @@ -765,7 +765,7 @@ def test_patch_dependent_field_on_origin_document(self): self.assert200(status) def test_patch_dependent_field_value_on_origin_document(self): - """ Test that when patching a field which is dependent on another field's + """Test that when patching a field which is dependent on another field's value, and this other field is not provided in the patch, but is present on the persisted document, the patch will be accepted. diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 461705f89..8844c8527 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -809,8 +809,8 @@ def test_post_readonly_field_with_default(self): self.assertValidationErrorStatus(status) def test_post_with_nested_default(self): - """ Test that in post of a field that has nested fields with default values - those default values are set + """Test that in post of a field that has nested fields with default values + those default values are set """ del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "dict_with_nested_default" diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index c00e4bd09..eed6493d0 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -198,9 +198,9 @@ def test_put_with_post_override(self): def test_put_sets_default_value_when_field_not_provided_neither_persisted(self): """ - Test that when replacing a document, any field that has default values - defined in the schema is set according to the schema default when - the current persisted document doesn't have the field value set. + Test that when replacing a document, any field that has default values + defined in the schema is set according to the schema default when + the current persisted document doesn't have the field value set. """ test_field = "unsetted_default_value_field" test_value = self.domain["contacts"]["schema"]["unsetted_default_value_field"][ @@ -213,12 +213,12 @@ def test_put_sets_default_value_when_field_not_provided_neither_persisted(self): def test_put_sets_default_value_when_field_not_provided_but_persisted(self): """ - Test that when replacing a document, any field that has default values - defined in the schema is set according to the schema default when - the current persisted document already had the field value set. + Test that when replacing a document, any field that has default values + defined in the schema is set according to the schema default when + the current persisted document already had the field value set. - This effectively makes impossible to delete fields with default values - in the schema using a PUT request. + This effectively makes impossible to delete fields with default values + in the schema using a PUT request. """ test_field = "title" test_value = "Mr." @@ -229,9 +229,9 @@ def test_put_sets_default_value_when_field_not_provided_but_persisted(self): def test_put_removes_non_provided_non_default_field(self): """ - Test that when replacing a document, any field that has doesn't have - a default value defined in the schema and has not been provided in - the request will be effectively deleted in the replaced version. + Test that when replacing a document, any field that has doesn't have + a default value defined in the schema and has not been provided in + the request will be effectively deleted in the replaced version. """ data = {"ref": "9234567890123456789054321"} diff --git a/eve/tests/renders.py b/eve/tests/renders.py index 0a84fa2ac..ad0fdacb1 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -43,8 +43,7 @@ def test_xml_leaf_escaping(self): self.assertTrue(b"12345 & 6789" in r.get_data()) def test_xml_ordered_nodes(self): - """ Test that xml nodes are ordered and #441 is addressed. - """ + """Test that xml nodes are ordered and #441 is addressed.""" r = self.test_client.get( "%s?max_results=1" % self.known_resource_url, headers=[("Accept", "application/xml")], diff --git a/eve/tests/utils.py b/eve/tests/utils.py index 82eadebee..cf06d631a 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -21,7 +21,7 @@ class TestUtils(TestBase): - """ collection, document and home_link methods (and resource_uri, which is + """collection, document and home_link methods (and resource_uri, which is used by all of them) are tested in 'tests.methods' since we need an active flaskapp context """ diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index ed499e19a..6f452ce97 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -187,7 +187,7 @@ def assertPrimaryAndShadowDocuments(self, _id, version, partial=False): self.assertEqual(len(shadow_document.keys()), num_meta_fields + 2) def assertHateoasLinks(self, links, version_param): - """ Makes sure links for `self`, `collection`, and `parent` point to + """Makes sure links for `self`, `collection`, and `parent` point to the right place. """ self_url = links["self"]["href"] @@ -306,46 +306,42 @@ def setUp(self): self.insertTestData() def test_get(self): - """ - """ + """""" self.do_test_get() def test_getitem(self): - """ - """ + """""" self.do_test_getitem(partial=False) def test_post(self): - """ Verify that a shadow document is created on post with all of the + """Verify that a shadow document is created on post with all of the appropriate fields. """ self.do_test_post(partial=False) def test_multi_post(self): - """ Eve literally throws single documents into an array before + """Eve literally throws single documents into an array before processing them in a POST, so I don't feel the need to specially test the versioning features here. Making a stub nontheless. """ self.do_test_multi_post() def test_put(self): - """ Verify that an additional shadow document is created on post with + """Verify that an additional shadow document is created on post with all of the appropriate fields. """ self.do_test_put(partial=False) def test_patch(self): - """ - """ + """""" self.do_test_patch(partial=False) def test_version_control_the_unkown(self): - """ - """ + """""" self.do_test_version_control_the_unkown() def test_getitem_version_unknown(self): - """ Make sure that Eve return a nice error when requesting an unknown + """Make sure that Eve return a nice error when requesting an unknown version. """ response, status = self.get( @@ -354,7 +350,7 @@ def test_getitem_version_unknown(self): self.assert404(status) def test_getitem_version_bad_format(self): - """ Make sure that Eve return a nice error when requesting an unknown + """Make sure that Eve return a nice error when requesting an unknown version. """ response, status = self.get( @@ -363,7 +359,7 @@ def test_getitem_version_bad_format(self): self.assert400(status) def test_getitem_version_all(self): - """ Verify that all documents are returned which each appearing exactly + """Verify that all documents are returned which each appearing exactly as it would if it were accessed explicitly. """ meta_fields = self.fields + [ @@ -418,7 +414,7 @@ def test_getitem_version_all(self): ) def test_getitem_version_pagination(self): - """ Verify that `?version=all` and `?version=diffs` display pagination + """Verify that `?version=all` and `?version=diffs` display pagination links when results exceed `PAGINATION_DEFAULT`. """ # create many versions @@ -447,7 +443,7 @@ def test_getitem_version_pagination(self): self.assertHateoasLinks(links, "all") def test_on_fetched_item(self): - """ Verify that on_fetched_item events are fired for versioned + """Verify that on_fetched_item events are fired for versioned requests. """ devent = DummyEvent(lambda: True) @@ -478,7 +474,7 @@ def test_on_fetched_item(self): self.assertEqual(None, devent.called) def test_on_fetched_item_contacts(self): - """ Verify that on_fetched_item_contacts events are fired for versioned + """Verify that on_fetched_item_contacts events are fired for versioned requests. """ devent = DummyEvent(lambda: True) @@ -509,7 +505,7 @@ def test_on_fetched_item_contacts(self): # TODO: also test with HATEOS off def test_on_fetched_diffs(self): - """ Verify that on_fetched_item events are fired for + """Verify that on_fetched_item events are fired for version=diffs requests. """ devent = DummyEvent(lambda: True) @@ -537,7 +533,7 @@ def test_on_fetched_diffs(self): self.assertEqual(2, len(devent.called)) def test_on_fetched_diffs_contacts(self): - """ Verify that on_fetched_diffs_contacts events are fired for + """Verify that on_fetched_diffs_contacts events are fired for version=diffs requests. """ devent = DummyEvent(lambda: True) @@ -568,7 +564,7 @@ def test_on_fetched_diffs_contacts(self): # TODO: also test with HATEOS off def test_getitem_version_diffs(self): - """ Verify that the first document is returned in its entirety and that + """Verify that the first document is returned in its entirety and that subsequent documents are simply diff to the previous version. """ meta_fields = self.fields + [ @@ -629,8 +625,7 @@ def test_getitem_version_diffs(self): # TODO: also test with HATEOS off def test_getitem_projection(self): - """ Verify that projections happen smoothly when versioning is on. - """ + """Verify that projections happen smoothly when versioning is on.""" # test inclusive projection response, status = self.get( self.known_resource, @@ -656,8 +651,7 @@ def test_getitem_projection(self): self.assertTrue(self.latest_version_field in response) def test_getitem_version_all_projection(self): - """ Verify that projections happen smoothly when versioning is on. - """ + """Verify that projections happen smoothly when versioning is on.""" # put a second version response, status = self.put( self.item_id_url, @@ -768,7 +762,7 @@ def test_getitem_version_new_latest_version_invalidates_if_none_match(self): self.assertEqual(document[self.latest_version_field], 2) def test_automatic_fields(self): - """ Make sure that Eve throws an error if we try to set a versioning + """Make sure that Eve throws an error if we try to set a versioning field manually. """ # set _version @@ -790,7 +784,7 @@ def test_automatic_fields(self): self.assertValidationError(r, {self.document_id_field: "unknown field"}) def test_referential_integrity(self): - """ Make sure that Eve still correctly handles vanilla data_relations + """Make sure that Eve still correctly handles vanilla data_relations when versioning is turned on. (Copied from tests/methods/post.py.) """ data = {"person": self.unknown_item_id} @@ -808,7 +802,7 @@ def test_referential_integrity(self): self.assert201(status) def test_delete(self): - """ Verify that we don't throw an error if we delete a resource that is + """Verify that we don't throw an error if we delete a resource that is supposed to be versioned but whose shadow collection does not exist. """ # turn off filter setting @@ -827,7 +821,7 @@ def test_delete(self): self.assertTrue(self.countShadowDocuments() == 0) def test_deleteitem(self): - """ Verify that we don't throw an error if we delete an item that is + """Verify that we don't throw an error if we delete an item that is supposed to be versioned but that doesn't have any shadow copies. """ # verify the primary document exists but no shadow documents do @@ -845,7 +839,7 @@ def test_deleteitem(self): self.assertTrue(self.countShadowDocuments(self.item_id) == 0) def test_softdelete(self): - """ Deleting a versioned item with soft delete enabled should create a + """Deleting a versioned item with soft delete enabled should create a new version marked as deleted, which is returned with 404 Not Found in response to GET requests. GETs of previous versions should continue to respond with `200 OK` responses. Requests for `?version=all/diff` @@ -916,7 +910,7 @@ def test_softdelete(self): self.assertTrue(field in items[1], "%s not in diffs" % field) def test_softdelete_version_db_fields(self): - """ Document versions created with soft delete enabled should include + """Document versions created with soft delete enabled should include the DELETED field. """ self.enableSoftDelete() @@ -963,7 +957,7 @@ def setUp(self): self.insertTestData() def test_referential_integrity(self): - """ Make sure that Eve correctly validates a data_relation with a + """Make sure that Eve correctly validates a data_relation with a version and returns the version with the data_relation in the response. """ data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] @@ -1061,7 +1055,7 @@ def test_referential_integrity(self): self.assertEqual(response["person"].get(version_field), 2) def test_embedded(self): - """ Perform a quick check to make sure that Eve can embedded with a + """Perform a quick check to make sure that Eve can embedded with a version in the data relation. """ data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] @@ -1083,7 +1077,7 @@ def test_embedded(self): self.assertTrue("ref" in response["person"]) def test_softdelete_embedded(self): - """ If a versioned embedded document is soft deleted, a previous + """If a versioned embedded document is soft deleted, a previous version should still resolve correctly. """ self.enableSoftDelete() @@ -1168,7 +1162,7 @@ def setUp(self): self.insertTestData() def test_referential_integrity(self): - """ Make sure that Eve correctly distinguishes between versions when + """Make sure that Eve correctly distinguishes between versions when referencing fields that aren't '_id'. """ # put a second version @@ -1220,7 +1214,7 @@ def setUp(self): self.insertTestData() def test_referential_integrity(self): - """ Make sure that Eve correctly distinguishes between versions when + """Make sure that Eve correctly distinguishes between versions when referencing unversioned fields """ # put a second version @@ -1259,41 +1253,38 @@ def setUp(self): self.insertTestData() def test_get(self): - """ Test that get response successfully synthesize the full document + """Test that get response successfully synthesize the full document even with unversioned fields. """ self.do_test_get() def test_getitem(self): - """ Test that get response can successfully synthesize both old and new + """Test that get response can successfully synthesize both old and new document versions when partial versioning is in place. """ self.do_test_getitem(partial=True) def test_post(self): - """ Verify that partial version control can happen on POST. - """ + """Verify that partial version control can happen on POST.""" self.do_test_post(partial=True) def test_multi_post(self): - """ Eve literally throws single documents into an array before + """Eve literally throws single documents into an array before processing them in a POST, so I don't feel the need to specially test the versioning features here. Making a stub nontheless. """ self.do_test_multi_post() def test_put(self): - """ Verify that partial version control can happen on PUT. - """ + """Verify that partial version control can happen on PUT.""" self.do_test_put(partial=True) def test_patch(self): - """ Verify that partial version control can happen on PATCH. - """ + """Verify that partial version control can happen on PATCH.""" self.do_test_patch(partial=True) def test_version_control_the_unkown(self): - """ Currently, the versioning scheme assumes true unless a field is + """Currently, the versioning scheme assumes true unless a field is explicitly marked to not be version controlled. That means, if 'allow_unknown' is enabled, those fields are always version controlled. This is the same behavior as under TestCompleteVersioning. @@ -1312,7 +1303,7 @@ def setUp(self): self.enableVersioning() def test_get(self): - """ Make sure that Eve returns version = 1 even for documents that + """Make sure that Eve returns version = 1 even for documents that haven't been modified since version control has been turned on. """ response, status = self.get(self.known_resource) @@ -1323,7 +1314,7 @@ def test_get(self): self.assertDocumentVersionFields(item, 1) def test_getitem(self): - """ Make sure that Eve returns version = 1 even for documents that + """Make sure that Eve returns version = 1 even for documents that haven't been modified since version control has been turned on. """ response, status = self.get(self.known_resource, item=self.item_id) @@ -1331,7 +1322,7 @@ def test_getitem(self): self.assertDocumentVersionFields(response, 1) def test_put(self): - """ Make sure that Eve jumps to version = 2 and saves two shadow copies + """Make sure that Eve jumps to version = 2 and saves two shadow copies (version 1 and version 2) for documents that where already in the database before version control was turned on. """ @@ -1354,7 +1345,7 @@ def test_put(self): self.assertEqual(response[ETAG], response2[ETAG]) def test_patch(self): - """ Make sure that Eve jumps to version = 2 and saves two shadow copies + """Make sure that Eve jumps to version = 2 and saves two shadow copies (version 1 and version 2) for documents that where already in the database before version control was turned on. """ @@ -1377,7 +1368,7 @@ def test_patch(self): self.assertEqual(response[ETAG], response2[ETAG]) def test_datasource(self): - """ Make sure that Eve uses the same mongo collection for storing versions + """Make sure that Eve uses the same mongo collection for storing versions when datasource is used.""" # make sure there are no shadow documents self.assertTrue(self.countShadowDocuments() == 0) @@ -1404,7 +1395,7 @@ def test_datasource(self): self.assertEqual(self.countShadowDocuments(), 3) def test_delete(self): - """ Verify that we don't throw an error if we delete a resource that is + """Verify that we don't throw an error if we delete a resource that is supposed to be versioned but whose shadow collection does not exist. """ # turn off filter setting @@ -1423,7 +1414,7 @@ def test_delete(self): self.assertTrue(self.countShadowDocuments() == 0) def test_deleteitem(self): - """ Verify that we don't throw an error if we delete an item that is + """Verify that we don't throw an error if we delete an item that is supposed to be versioned but that doesn't have any shadow copies. """ # verify the primary document exists but no shadow documents do @@ -1441,7 +1432,7 @@ def test_deleteitem(self): self.assertTrue(self.countShadowDocuments(self.item_id) == 0) def test_softdelete(self): - """ Make sure that Eve jumps to version = 2 and saves two shadow copies + """Make sure that Eve jumps to version = 2 and saves two shadow copies (version 1 and version 2) for documents that where already in the database before version control was turned on. """ @@ -1463,7 +1454,7 @@ def test_softdelete(self): self.assertTrue(self.countShadowDocuments(self.item_id) == 2) def test_referential_integrity(self): - """ Make sure that Eve doesn't mind doing a data relation even when the + """Make sure that Eve doesn't mind doing a data relation even when the shadow copy doesn't exist. """ data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] @@ -1476,7 +1467,7 @@ def test_referential_integrity(self): self.assert201(status) def test_embedded(self): - """ Perform a quick check to make sure that Eve can embedded with a + """Perform a quick check to make sure that Eve can embedded with a version in the data relation. """ data_relation = self.domain["invoices"]["schema"]["person"]["data_relation"] @@ -1507,6 +1498,5 @@ def setUp(self): self.insertTestData() def test_getitem(self): - """ Make sure we can insert at least two versioning documents. - """ + """Make sure we can insert at least two versioning documents.""" self.do_test_getitem(partial=False) diff --git a/eve/utils.py b/eve/utils.py index 4498edc9f..d1cc8505a 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -26,7 +26,7 @@ class Config(object): - """ Helper class used through the code to access configuration settings. + """Helper class used through the code to access configuration settings. If the main flaskapp object is not instantiated yet, returns the default setting in the eve __init__.py module, otherwise returns the flaskapp config value (which value might override the static defaults). @@ -48,7 +48,7 @@ def __getattr__(self, name): class ParsedRequest(object): - """ This class, by means of its attributes, describes a client request. + """This class, by means of its attributes, describes a client request. .. versionchanged:: 9,5 'args' keyword. @@ -100,7 +100,7 @@ class ParsedRequest(object): def parse_request(resource): - """ Parses a client request, returning instance of :class:`ParsedRequest` + """Parses a client request, returning instance of :class:`ParsedRequest` containing relevant request data. :param resource: the resource currently being accessed by the client. @@ -182,7 +182,7 @@ def etag_parse(challenge): def weak_date(date): - """ Returns a RFC-1123 string corresponding to a datetime value plus + """Returns a RFC-1123 string corresponding to a datetime value plus a 1 second timedelta. This is needed because when saved, documents LAST_UPDATED values have higher resolution than If-Modified-Since's, which is limited to seconds. @@ -197,7 +197,7 @@ def weak_date(date): def str_to_date(string): - """ Converts a date string formatted as defined in the configuration + """Converts a date string formatted as defined in the configuration to the corresponding datetime value. :param string: the RFC-1123 string to convert to datetime value. @@ -206,7 +206,7 @@ def str_to_date(string): def date_to_str(date): - """ Converts a datetime value to the format defined in the configuration file. + """Converts a datetime value to the format defined in the configuration file. :param date: the datetime value to convert. """ @@ -214,7 +214,7 @@ def date_to_str(date): def date_to_rfc1123(date): - """ Converts a datetime value to the corresponding RFC-1123 string. + """Converts a datetime value to the corresponding RFC-1123 string. :param date: the datetime value to convert. """ @@ -222,7 +222,7 @@ def date_to_rfc1123(date): def home_link(): - """ Returns a link to the API entry point/home page. + """Returns a link to the API entry point/home page. .. versionchanged:: 0.5 Link is relative to API root. @@ -234,7 +234,7 @@ def home_link(): def api_prefix(url_prefix=None, api_version=None): - """ Returns the prefix to API endpoints, according to the URL_PREFIX and + """Returns the prefix to API endpoints, according to the URL_PREFIX and API_VERSION configuration settings. :param url_prefix: the prefix string. If `None`, defaults to the current @@ -269,7 +269,7 @@ def querydef( page=None, other_params=MultiDict(), ): - """ Returns a valid query string. + """Returns a valid query string. :param max_results: `max_result` part of the query string. Defaults to `PAGINATION_DEFAULT` @@ -323,7 +323,7 @@ def querydef( def document_etag(value, ignore_fields=None): - """ Computes and returns a valid ETag for the input value. + """Computes and returns a valid ETag for the input value. :param value: the value to compute the ETag with. :param ignore_fields: `ignore_fields` list of fields to skip to @@ -366,7 +366,7 @@ def filter_ignore_fields(d, fields): def extract_key_values(key, d): - """ Extracts all values that match a key, even in nested dicts. + """Extracts all values that match a key, even in nested dicts. :param key: the lookup key. :param d: the dict to scan. @@ -382,7 +382,7 @@ def extract_key_values(key, d): def debug_error_message(msg): - """ Returns the error message `msg` if config.DEBUG is True + """Returns the error message `msg` if config.DEBUG is True otherwise returns `None` which will cause Werkzeug to provide a generic error message @@ -396,7 +396,7 @@ def debug_error_message(msg): def validate_filters(where, resource): - """ Report any filter which is not allowed by `allowed_filters` + """Report any filter which is not allowed by `allowed_filters` :param where: the where clause, as a dict. :param resource: the resource being inspected. @@ -499,7 +499,7 @@ def recursive_validate_filter(key, value, schema): def auto_fields(resource): - """ Returns a list of automatically handled fields for a resource. + """Returns a list of automatically handled fields for a resource. :param resource: the resource currently being accessed by the client. @@ -537,9 +537,7 @@ def auto_fields(resource): def import_from_string(module_name): - """ Imports module using string - - """ + """Imports module using string""" try: modules = module_name.split(".") module_path, attr = ".".join(modules[:-1]), modules[-1] diff --git a/eve/validation.py b/eve/validation.py index 4744b1e5d..973e9f61c 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -31,7 +31,7 @@ def __init__(self, *args, **kwargs): def validate_update( self, document, document_id, persisted_document=None, normalize_document=True ): - """ Validate method to be invoked when performing an update, not an + """Validate method to be invoked when performing an update, not an insert. :param document: the document to be validated. @@ -47,7 +47,7 @@ def validate_update( ) def validate_replace(self, document, document_id, persisted_document=None): - """ Validation method to be invoked when performing a document + """Validation method to be invoked when performing a document replacement. This differs from :func:`validation_update` since in this case we want to perform a full :func:`validate` (the new document is to be considered a new insertion and required fields needs validation). @@ -89,10 +89,10 @@ def _normalize_default(self, mapping, schema, field): super(Validator, self)._normalize_default(mapping, schema, field) def _normalize_default_setter(self, mapping, schema, field): - """ {'oneof': [ - {'type': 'callable'}, - {'type': 'string'} - ]} """ + """{'oneof': [ + {'type': 'callable'}, + {'type': 'string'} + ]}""" if not self.persisted_document or field not in self.persisted_document: super(Validator, self)._normalize_default_setter(mapping, schema, field) @@ -153,7 +153,7 @@ def persisted_document(self, value): class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler): - """ Default Cerberus error handler for Eve. + """Default Cerberus error handler for Eve. Since Cerberus 1.0, error messages for fields will always be returned as lists, even in the case of a single error. To maintain compatibility with diff --git a/eve/versioning.py b/eve/versioning.py index ca9ab8437..8eff8cba4 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -4,7 +4,7 @@ def versioned_id_field(resource_settings): - """ Shorthand to add two commonly added versioning parameters. + """Shorthand to add two commonly added versioning parameters. .. versionadded: 0.4 """ @@ -12,7 +12,7 @@ def versioned_id_field(resource_settings): def resolve_document_version(document, resource, method, latest_doc=None): - """ Version number logic for all methods. + """Version number logic for all methods. :param document: the document in question. :param resource: the resource of the request/document. @@ -78,7 +78,7 @@ def resolve_document_version(document, resource, method, latest_doc=None): def late_versioning_catch(document, resource): - """ Insert versioning copy of document for the previous version of a + """Insert versioning copy of document for the previous version of a document if it is missing. Intended for PUT and PATCH. :param resource: the resource of the request/document. @@ -104,7 +104,7 @@ def late_versioning_catch(document, resource): def insert_versioning_documents(resource, documents): - """ Insert versioning copy of document. Intended for POST, PUT, and PATCH. + """Insert versioning copy of document. Intended for POST, PUT, and PATCH. :param resource: the resource of the request/document. :param documents: the documents be written by POST, PUT, or PATCH. @@ -160,7 +160,7 @@ def insert_versioning_documents(resource, documents): def versioned_fields(resource_def): - """ Returns a list of versioned fields for a resource. + """Returns a list of versioned fields for a resource. :param resource_def: a resource definition. @@ -191,7 +191,7 @@ def versioned_fields(resource_def): def diff_document(resource_def, old_doc, new_doc): - """ Returns a list of added or modified fields. + """Returns a list of added or modified fields. :param resource_def: a resource definition. :param old_doc: the document to compare against. @@ -228,7 +228,7 @@ def diff_document(resource_def, old_doc, new_doc): def synthesize_versioned_document(document, delta, resource_def): - """ Synthesizes a versioned document from the latest document and the + """Synthesizes a versioned document from the latest document and the values of all versioned fields from the old version. This is accomplished by first creating a new document with only the un-versioned fields of latest document, before updating with versioned fields from the old @@ -270,7 +270,7 @@ def synthesize_versioned_document(document, delta, resource_def): def get_old_document(resource, req, lookup, document, version): - """ Returns an old document if appropriate, otherwise returns a shallow + """Returns an old document if appropriate, otherwise returns a shallow copy of the given document. :param resource: the name of the resource. @@ -318,7 +318,7 @@ def get_old_document(resource, req, lookup, document, version): def get_data_version_relation_document(data_relation, reference, latest=False): - """ Returns document at the version specified in data_relation, or at the + """Returns document at the version specified in data_relation, or at the latest version if passed `latest=True`. Returns None if data_relation cannot be satisfied. @@ -387,7 +387,7 @@ def get_data_version_relation_document(data_relation, reference, latest=False): def missing_version_field(data_relation, reference): - """ Returns a document if it matches the value_field but doesn't have a + """Returns a document if it matches the value_field but doesn't have a _version field. This is the scenario when there is data in the database before document versioning is turned on. From edebdfbcb7dbd278e3a2f66ac83a2058179f4b7a Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Tue, 22 Sep 2020 17:52:17 +0200 Subject: [PATCH 646/821] Fix the mongo_options work by passing the options. This is done by passing the options directly into the find_one method. The work doesn't cover adding arguments to find_one_raw or find since they aren't needed to fix the bug. I explored adding a fluid type interface for with_optios to the eve PyMongos/Mongo/DataLayer classes but it was really clunky and I'd be worried that such a fundamental change wouldn't be appropriate here. --- eve/io/base.py | 4 +++- eve/io/mongo/mongo.py | 10 +++++++--- eve/methods/common.py | 13 ++++--------- eve/methods/patch.py | 9 +++------ eve/tests/methods/patch.py | 20 ++++++++++++++++++++ 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/eve/io/base.py b/eve/io/base.py index ac7b287a4..8aa2e5440 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -163,6 +163,7 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, + mongo_options=None, **lookup ): """Retrieves a single document/record. Consumed when a request hits an @@ -185,7 +186,8 @@ def find_one( include the user-restricted resource access field (if configured). Defaults to ``False``. - + :param mongo_options: options to pass to PyMongo. e.g. read_preferences + of the initial get. :param **lookup: the lookup fields. This will most likely be a record id or, if alternate lookup is supported by the API, the corresponding query. diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 41093a78c..174b5ab28 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -297,12 +297,14 @@ def find_one( req, check_auth_value=True, force_auth_field_projection=False, + mongo_options=None, **lookup ): """Retrieves a single document. :param resource: resource name. :param req: a :class:`ParsedRequest` instance. + :param mongo_options: Dict of parameters to pass to PyMongo with_options. :param **lookup: lookup query. .. versionchanged:: 0.6 @@ -345,9 +347,11 @@ def find_one( ): filter_ = self.combine_queries(filter_, {config.DELETED: {"$ne": True}}) # Here, we feed pymongo with `None` if projection is empty. - return ( - self.pymongo(resource).db[datasource].find_one(filter_, projection or None) - ) + target = self.pymongo(resource).db[datasource] + if mongo_options: + return target.with_options(**mongo_options).find_one(filter_, projection or None) + else: + return target.find_one(filter_, projection or None) def find_one_raw(self, resource, **lookup): """Retrieves a single raw document. diff --git a/eve/methods/common.py b/eve/methods/common.py index 50dcc33f8..2f749b8e1 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -61,7 +61,7 @@ def get_document( the user-restricted resource access field (if configured). Defaults to ``False``. - :param mongo_options: Options to pass to PyMongo. e.g. ReadConcern + :param mongo_options: Options to pass to PyMongo. e.g. read_preferences. :param **lookup: document lookup query .. versionchanged:: 0.6 @@ -87,14 +87,9 @@ def get_document( if original: document = original else: - if mongo_options: - document = app.data.with_options(mongo_options).find_one( - resource, req, check_auth_value, force_auth_field_projection, **lookup - ) - else: - document = app.data.find_one( - resource, req, check_auth_value, force_auth_field_projection, **lookup - ) + document = app.data.find_one( + resource, req, check_auth_value, force_auth_field_projection, mongo_options=mongo_options, **lookup + ) if document: e_if_m = config.ENFORCE_IF_MATCH diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 891447ac4..a42db072f 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -80,7 +80,7 @@ def patch_internal( option, a request context must be available. :param concurrency_check: concurrency check switch (bool) :param skip_validation: skip payload validation before write (bool) - :param mongo_options: options to pass to PyMongo. e.g. ReadConcern of the initial get. + :param mongo_options: options to pass to PyMongo. e.g. read_preferences of the initial get. :param **lookup: document lookup query. .. versionchanged:: 0.6.2 @@ -151,7 +151,7 @@ def patch_internal( if payload is None: payload = payload_() - original = get_document(resource, concurrency_check, mongo_options, **lookup) + original = get_document(resource, concurrency_check, mongo_options=mongo_options, **lookup) if not original: # not found abort(404) @@ -219,10 +219,7 @@ def patch_internal( if resource_def["merge_nested_documents"]: updates = resolve_nested_documents(updates, updated) - if mongo_options: - updated.with_options(mongo_options).update(updates) - else: - updated.update(updates) + updated.update(updates) if config.IF_MATCH: resolve_document_etag(updated, resource) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index d72741378..94b739941 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -1,6 +1,8 @@ import simplejson as json from bson import ObjectId +from pymongo import ReadPreference + from eve import ETAG from eve import ISSUES from eve import LAST_UPDATED @@ -305,6 +307,24 @@ def test_patch_internal(self): self.assertEqual(db_value, test_value) self.assert200(status) + def test_patch_internal_with_options(self): + # test that patch_internal is available and working properly. + test_field = "ref" + test_value = "9876543210987654321098765" + data = {test_field: test_value} + mongo_options = {'read_preference': ReadPreference.PRIMARY} + with self.app.test_request_context(self.item_id_url): + r, _, _, status = patch_internal( + self.known_resource, + data, + concurrency_check=False, + mongo_options=mongo_options, + **{"_id": self.item_id} + ) + db_value = self.compare_patch_with_get(test_field, r) + self.assertEqual(db_value, test_value) + self.assert200(status) + def test_patch_etag_header(self): # test that Etag is always included with response header. See #562. changes = {"ref": "1234567890123456789012345"} From f196366dc22a7ea51f14def4cb611bc1be741cb9 Mon Sep 17 00:00:00 2001 From: Ewan Higgs Date: Mon, 5 Oct 2020 14:24:47 +0200 Subject: [PATCH 647/821] Fix linting errors. --- eve/io/mongo/mongo.py | 4 +++- eve/methods/common.py | 7 ++++++- eve/methods/patch.py | 4 +++- eve/tests/methods/patch.py | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 174b5ab28..97d49b8f4 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -349,7 +349,9 @@ def find_one( # Here, we feed pymongo with `None` if projection is empty. target = self.pymongo(resource).db[datasource] if mongo_options: - return target.with_options(**mongo_options).find_one(filter_, projection or None) + return target.with_options(**mongo_options).find_one( + filter_, projection or None + ) else: return target.find_one(filter_, projection or None) diff --git a/eve/methods/common.py b/eve/methods/common.py index 2f749b8e1..f5c71dc16 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -88,7 +88,12 @@ def get_document( document = original else: document = app.data.find_one( - resource, req, check_auth_value, force_auth_field_projection, mongo_options=mongo_options, **lookup + resource, + req, + check_auth_value, + force_auth_field_projection, + mongo_options=mongo_options, + **lookup ) if document: diff --git a/eve/methods/patch.py b/eve/methods/patch.py index a42db072f..2589b352a 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -151,7 +151,9 @@ def patch_internal( if payload is None: payload = payload_() - original = get_document(resource, concurrency_check, mongo_options=mongo_options, **lookup) + original = get_document( + resource, concurrency_check, mongo_options=mongo_options, **lookup + ) if not original: # not found abort(404) diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 94b739941..6a4d2b690 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -312,7 +312,7 @@ def test_patch_internal_with_options(self): test_field = "ref" test_value = "9876543210987654321098765" data = {test_field: test_value} - mongo_options = {'read_preference': ReadPreference.PRIMARY} + mongo_options = {"read_preference": ReadPreference.PRIMARY} with self.app.test_request_context(self.item_id_url): r, _, _, status = patch_internal( self.known_resource, From cc0c3c38464fbbd28a09f11ef0053798d79139ee Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 09:43:10 +0200 Subject: [PATCH 648/821] changelog for #1413 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 055d64e09..5a1aeb760 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Fix: Use ``**mongo_options`` in ``with_options`` (`#1413`_) + +.. _`#1413`: https://github.com/pyeve/eve/issues/1413 Version 1.1.3 ------------- From 3424efa4e5dd3c512261f64890e7605f5f4d6835 Mon Sep 17 00:00:00 2001 From: Tadej Magajna Date: Sun, 18 Oct 2020 01:05:07 +0200 Subject: [PATCH 649/821] Expose media endpoint only if RETURN_MEDIA_AS_URL is True --- eve/flaskapp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 36dfd0d64..29218d9a0 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -180,7 +180,10 @@ def __init__( self.auth = None self._init_url_rules() - self._init_media_endpoint() + + if self.config["RETURN_MEDIA_AS_URL"]: + self._init_media_endpoint() + self._init_schema_endpoint() if self.config["OPLOG"] is True: From 935bdb01a65eba59afc8cd661730421b81b04087 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 09:50:36 +0200 Subject: [PATCH 650/821] changelog for #1415 --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5a1aeb760..7860b2dbf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Fix: Use ``**mongo_options`` in ``with_options`` (`#1413`_) +Fixed +~~~~~ + +- Expose media endpoint only if ``RETURN_MEDIA_AS_URL`` is set to ``True`` (`#1415`_) +- Use ``**mongo_options`` in ``with_options`` (`#1413`_) +.. _`#1415`: https://github.com/pyeve/eve/pull/1415 .. _`#1413`: https://github.com/pyeve/eve/issues/1413 Version 1.1.3 From d8dfc2d37beb74a2f3777702547364d4316d1b37 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 09:51:17 +0200 Subject: [PATCH 651/821] Tadej Magajn --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8636c7c1b..52e514427 100644 --- a/AUTHORS +++ b/AUTHORS @@ -178,6 +178,7 @@ Patches and Contributions - Stefaan Ghysels - Stratos Gerakakis - Sybren A. Stüvel +- Tadej Magajn - Tano Abeleyra - Taylor Brown - Thomas Sileo From 870e60e22f519d0289d55af4bf3200ba5f825a9d Mon Sep 17 00:00:00 2001 From: pramos Date: Tue, 20 Oct 2020 08:54:10 +0200 Subject: [PATCH 652/821] Added test schemas with nested dict --- eve/tests/test_settings.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 5dae1f5e3..4a1b70efb 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -329,6 +329,37 @@ }, } +brands = { + "item_title": "brand", + "schema": { + "name": {"type": "string"}, + "address": "string", + } +} + +components = { + "item_title": "component", + "schema": { + "name": {"type": "string"}, + "price": "integer", + "brand": {"type": "objectid", "data_relation": {"resource": "brands"}}, + } +} + +computers = { + "item_title": "computers", + "schema": { + "name": {"type": "string"}, + "components": { + "type": "dict", + "schema": { + "cpu": {"type": "objectid", "data_relation": {"resource": "components"}}, + "motherboard": {"type": "objectid", "data_relation": {"resource": "components"}}, + } + } + } +} + child_products = copy.deepcopy(products) child_products["url"] = 'products//children' child_products["datasource"] = {"source": "products"} @@ -367,4 +398,7 @@ "tenant_b": tenant_b, "test_unique": test_unique, "credit_rules": credit_rules, + "brands": brands, + "components": components, + "computers": computers, } From 8bc4df4de29a76adfca90f3d3c25cfbdc0ddfb48 Mon Sep 17 00:00:00 2001 From: jjimenez Date: Tue, 20 Oct 2020 08:59:51 +0200 Subject: [PATCH 653/821] Added tests to reproduce issue #1416 --- eve/tests/methods/get.py | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 09ce7c45a..2fb52e963 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1098,6 +1098,96 @@ def test_get_reference_embedded_in_subdocuments(self): "location" in content["_items"][0]["departments"][0]["members"][0] ) + def test_get_reference_embedded_in_subdocuments_with_nested_dicts(self): + _db = self.connection[MONGO_DBNAME] + cpu_brand_name = self.random_string(10) + cpu_brand = { + "name": cpu_brand_name, + "address": self.random_string(30), + } + motherboard_brand_name = self.random_string(15) + motherboard_brand = { + "name": motherboard_brand_name, + "address": self.random_string(30), + } + cpu_brand_id, motherboard_brand_id = _db.brands.insert_many( + [cpu_brand, motherboard_brand] + ).inserted_ids + cpu_component = { + "name": self.random_string(12), + "price": 499, + "brand": cpu_brand_id, + } + motherboard_component = { + "name": self.random_string(18), + "price": 199, + "brand": motherboard_brand_id, + } + cpu_component_id, motherboard_component_id = _db.components.insert_many( + [cpu_component, motherboard_component] + ).inserted_ids + computer = { + "name": self.random_string(25), + "components": { + "cpu": cpu_component_id, + "motherboard": motherboard_component_id, + }, + } + computer_id = _db.computers.insert_one(computer).inserted_id + computers = self.domain["computers"] + components = self.domain["components"] + # Test that doesn't come embedded if asking for a field that + # isn't embedded ('embeddable' is False by default) + embedded = ( + '{"components.cpu": 1, "components.motherboard": 1,' + + ' "components.cpu.brand": 1, "components.motherboard.brand": 1}' + ) + result = self.test_client.get( + "%s/%s/%s" % (computers["url"], computer_id, "?embedded=%s" % embedded) + ) + self.assert200(result.status_code) + content = json.loads(result.get_data()) + self.assertEqual(content["components"]["cpu"], str(cpu_component_id)) + self.assertEqual( + content["components"]["motherboard"], str(motherboard_component_id) + ) + # Set field to be embedded + computers["schema"]["components"]["schema"]["cpu"]["data_relation"][ + "embeddable" + ] = True + computers["schema"]["components"]["schema"]["motherboard"]["data_relation"][ + "embeddable" + ] = True + components["schema"]["brand"]["data_relation"]["embeddable"] = True + # Test that global setting applies even if field is set to embedded + computers["embedding"] = False + components["embedding"] = False + result = self.test_client.get( + "%s/%s/%s" % (computers["url"], computer_id, "?embedded=%s" % embedded) + ) + self.assert200(result.status_code) + content = json.loads(result.get_data()) + self.assertEqual(content["components"]["cpu"], str(cpu_component_id)) + self.assertEqual( + content["components"]["motherboard"], str(motherboard_component_id) + ) + # Test that it works + computers["embedding"] = True + components["embedding"] = True + result = self.test_client.get( + "%s/%s/%s" % (computers["url"], computer_id, "?embedded=%s" % embedded) + ) + self.assert200(result.status_code) + content = json.loads(result.get_data()) + self.assertEqual( + content["components"]["cpu"]["brand"]["name"], + cpu_brand_name, + ) + self.assertEqual( + content["components"]["motherboard"]["brand"]["name"], + motherboard_brand_name, + ) + def test_get_nested_resource(self): response, status = self.get("users/overseas") self.assertGet(response, status, "users_overseas") From 648870ae54c742be32b92d7c0e6c031a9ab330a4 Mon Sep 17 00:00:00 2001 From: jjimenez Date: Tue, 20 Oct 2020 09:08:26 +0200 Subject: [PATCH 654/821] Reformated file to pass linting --- eve/tests/test_settings.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 4a1b70efb..ca7c249eb 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -334,7 +334,7 @@ "schema": { "name": {"type": "string"}, "address": "string", - } + }, } components = { @@ -343,7 +343,7 @@ "name": {"type": "string"}, "price": "integer", "brand": {"type": "objectid", "data_relation": {"resource": "brands"}}, - } + }, } computers = { @@ -353,11 +353,17 @@ "components": { "type": "dict", "schema": { - "cpu": {"type": "objectid", "data_relation": {"resource": "components"}}, - "motherboard": {"type": "objectid", "data_relation": {"resource": "components"}}, - } - } - } + "cpu": { + "type": "objectid", + "data_relation": {"resource": "components"}, + }, + "motherboard": { + "type": "objectid", + "data_relation": {"resource": "components"}, + }, + }, + }, + }, } child_products = copy.deepcopy(products) From 13ca22763a2190572442015f0aa29098f6b8a837 Mon Sep 17 00:00:00 2001 From: jjimenez Date: Tue, 20 Oct 2020 09:09:37 +0200 Subject: [PATCH 655/821] Added prefix for refs in nested dicts --- eve/methods/common.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index f5c71dc16..9e39df5b3 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1040,13 +1040,14 @@ def add_query_to_list(query, subresource, subresource_query): query["$or"] = [] -def subdocuments(fields_chain, resource, document): +def subdocuments(fields_chain, resource, document, prefix=""): """Traverses the given document and yields subdocuments which correspond to the given fields_chain :param fields_chain: list of nested field names. :param resource: the resource name. :param document: document to be traversed + :param prefix: prefix to recursively concatenate nested field names. .. versionadded:: 0.5 """ @@ -1056,14 +1057,17 @@ def subdocuments(fields_chain, resource, document): subdocument = document[fields_chain[0]] docs = subdocument if isinstance(subdocument, list) else [subdocument] try: - resource = field_definition(resource, fields_chain[0])["data_relation"][ - "resource" - ] + definition = field_definition(resource, prefix + fields_chain[0]) + if "data_relation" in definition: + resource = definition["data_relation"]["resource"] + prefix = "" + else: + prefix = prefix + fields_chain[0] + "." except KeyError: resource = resource for doc in docs: - for result in subdocuments(fields_chain[1:], resource, doc): + for result in subdocuments(fields_chain[1:], resource, doc, prefix): yield result else: yield document From 28a662b6ed2eedfcaeac97cf7e682c4a4a3748b4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 09:58:34 +0200 Subject: [PATCH 656/821] =?UTF-8?q?Patricia=20Ramos;=20Javier=20Jim=C3=A9n?= =?UTF-8?q?ez?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS | 2 ++ CHANGES.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS b/AUTHORS index 52e514427..1f2ad765a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -78,6 +78,7 @@ Patches and Contributions - James Stewart - Jaroslav Semančík - Javier Gonel +- Javier Jiménez - Jean Boussier - Jen Montes - Jeremy Solbrig @@ -143,6 +144,7 @@ Patches and Contributions - Or Neeman - Orange Tsai - Pahaz Blinov +- Patricia Ramos - Patrick Decat - Pau Freixes - Paul Doucet diff --git a/CHANGES.rst b/CHANGES.rst index 7860b2dbf..84e0440f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,9 +9,11 @@ In Development Fixed ~~~~~ +- Error raised when using ``embedded`` with nested dict (`#1416`_) - Expose media endpoint only if ``RETURN_MEDIA_AS_URL`` is set to ``True`` (`#1415`_) - Use ``**mongo_options`` in ``with_options`` (`#1413`_) +.. _`#1416`: https://github.com/pyeve/eve/issues/1416 .. _`#1415`: https://github.com/pyeve/eve/pull/1415 .. _`#1413`: https://github.com/pyeve/eve/issues/1413 From 400f7a58075f91efe4bae7f759d9dbde661bdd3b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 10:09:01 +0200 Subject: [PATCH 657/821] bump version to 1.1.4 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 84e0440f8..115e6450c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones + +Version 1.1.4 +------------- + +Released on October 22, 2020. + Fixed ~~~~~ diff --git a/eve/__init__.py b/eve/__init__.py index 0a90a588a..f3ae5f5df 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.3" +__version__ = "1.1.4" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From e63d645cb645477e563ba3aa49db00eb1c7c5b2f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 22 Oct 2020 10:10:18 +0200 Subject: [PATCH 658/821] bump versiont to 1.1.5.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index f3ae5f5df..fb307c677 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.4" +__version__ = "1.1.5.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 24f13e41a9fe2d2efef03ba82e680143a6cbc56d Mon Sep 17 00:00:00 2001 From: alexmisk Date: Tue, 27 Oct 2020 21:22:42 +0300 Subject: [PATCH 659/821] Instantiate GridFS object with 'disable_md5=True' (fix #1410) --- eve/io/mongo/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 07883a0a0..62b78ebf1 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -59,7 +59,7 @@ def fs(self, resource=None): px = driver.current_mongo_prefix(resource) if px not in self._fs: - self._fs[px] = GridFS(driver.pymongo(prefix=px).db) + self._fs[px] = GridFS(driver.pymongo(prefix=px).db, disable_md5=True) return self._fs[px] def get(self, _id, resource=None): From e432ecbdf10dfaa869e1c23ed79e39845b5af0c0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 31 Oct 2020 08:50:06 +0100 Subject: [PATCH 660/821] Changelog for #1419 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 115e6450c..183ccaa0d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones +- Disable MD5 support in GridFS, as it is deprecated (`#1410`_). + +.. _`#1410`: https://github.com/pyeve/eve/issues/1410 Version 1.1.4 ------------- From 452fdfa5a700c0116e3a1fd342da3e10893f272c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 31 Oct 2020 08:50:14 +0100 Subject: [PATCH 661/821] Alexander Miskaryan --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 1f2ad765a..b7098a1a3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Patches and Contributions - Alex Misk - Alexander Dietmüller - Alexander Hendorf +- Alexander Miskaryan - Amedeo Bussi - Andreas Røssland - Andrés Martano From f029bd6e191aa290922350b34ffd52ea9ca95625 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Nov 2020 20:44:02 +0100 Subject: [PATCH 662/821] drop demo references from docs --- CHANGES.rst | 1 + docs/config.rst | 7 +++--- docs/features.rst | 56 ++++++++++++++++++++++----------------------- docs/index.rst | 17 -------------- docs/validation.rst | 4 ++-- 5 files changed, 33 insertions(+), 52 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 183ccaa0d..d75db4425 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ In Development --------------- - Disable MD5 support in GridFS, as it is deprecated (`#1410`_). +- Demo application has been terminated by Heroky; dropped any reference to it. .. _`#1410`: https://github.com/pyeve/eve/issues/1410 diff --git a/docs/config.rst b/docs/config.rst index d58a34bab..79763b49d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -66,8 +66,7 @@ There are many alternative ways to handle development/production however. Using Python modules for configuration is very convenient, as they allow for all kinds of nice tricks, like being able to seamlessly launch the same API on both local and production systems, connecting to the appropriate -database instance as needed. Consider the following example, taken directly -from the :ref:`demo`: +database instance as needed. Consider the following example: :: @@ -1590,7 +1589,7 @@ exposed even by client-side projection. The following API call will not return .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?projection={"lastname": 1, "born": 1} + $ curl -i http://myapi/people?projection={"lastname": 1, "born": 1} HTTP/1.1 200 OK You can also exclude fields from API responses. But this time, the excluded @@ -1615,7 +1614,7 @@ fields returned can be defined by short-cut functions from client-side. .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?projection={"username": 1} + $ curl -i http://myapi/people?projection={"username": 1} HTTP/1.1 200 OK diff --git a/docs/features.rst b/docs/features.rst index 64d837dd2..e722b1d7b 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1,8 +1,6 @@ Features ======== -Below is a list of main features that any EVE-powered APIs can expose. Most of -these features can be experienced live by consuming the Demo API (see -:ref:`demo`). +Below is a list of main features that any EVE-powered APIs can expose. Emphasis on REST ---------------- @@ -49,7 +47,7 @@ can customize the URIs though, so the API endpoint could become, say, .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people + $ curl -i http://myapi.com/people HTTP/1.1 200 OK The response payload will look something like this: @@ -223,7 +221,7 @@ primary endpoint and will match your database primary key structure (i.e., an .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c + $ curl -i http://myapi.com/people/521d6840c437dc0002d1203c HTTP/1.1 200 OK Etag: 28995829ee85d69c4c18d597a0f68ae606a266cc Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT @@ -234,7 +232,7 @@ will retrieve only the first match anyway. .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people/Doe + $ curl -i http://myapi.com/people/Doe HTTP/1.1 200 OK Etag: 28995829ee85d69c4c18d597a0f68ae606a266cc Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT @@ -288,26 +286,26 @@ Here we are asking for all documents where ``lastname`` value is ``Doe``: :: - http://eve-demo.herokuapp.com/people?where={"lastname": "Doe"} + http://myapi.com/people?where={"lastname": "Doe"} With ``curl`` you would go like this: .. code-block:: console - $ curl -i -g http://eve-demo.herokuapp.com/people?where={%22lastname%22:%20%22Doe%22} + $ curl -i -g http://myapi.com/people?where={%22lastname%22:%20%22Doe%22} HTTP/1.1 200 OK Filtering on embedded document fields is possible: :: - http://eve-demo.herokuapp.com/people?where={"location.city": "San Francisco"} + http://myapi.com/people?where={"location.city": "San Francisco"} Date fields are also easy to query on: :: - http://eve-demo.herokuapp.com/people?where={"born": {"$gte":"Wed, 25 Feb 1987 17:00:00 GMT"}} + http://myapi.com/people?where={"born": {"$gte":"Wed, 25 Feb 1987 17:00:00 GMT"}} Date values should conform to RFC1123. Should you need a different format, you can change the ``DATE_FORMAT`` setting. @@ -318,7 +316,7 @@ Native Python syntax works like this: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?where=lastname=="Doe" + $ curl -i http://myapi.com/people?where=lastname=="Doe" HTTP/1.1 200 OK Both syntaxes allow for conditional and logical And/Or operators, however @@ -341,7 +339,7 @@ You can pretty print the response by specifying a query parameter named .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?pretty + $ curl -i http://myapi.com/people?pretty HTTP/1.1 200 OK { @@ -379,7 +377,7 @@ Sorting is supported as well: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?sort=city,-lastname + $ curl -i http://myapi.com/people?sort=city,-lastname HTTP/1.1 200 OK Would return documents sorted by city and then by lastname (descending). As you @@ -390,13 +388,13 @@ The MongoDB data layer also supports native MongoDB syntax: :: - http://eve-demo.herokuapp.com/people?sort=[("lastname", -1)] + http://myapi.com/people?sort=[("lastname", -1)] which translates to the following ``curl`` request: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?sort=[(%22lastname%22,%20-1)] + $ curl -i http://myapi.com/people?sort=[(%22lastname%22,%20-1)] HTTP/1.1 200 OK Would return documents sorted by lastname in descending order. @@ -423,14 +421,14 @@ consumers can request specific pages via the query string: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?max_results=20&page=2 + $ curl -i http://myapi.com/people?max_results=20&page=2 HTTP/1.1 200 OK Of course you can mix all the available query parameters: .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com/people?where={"lastname": "Doe"}&sort=[("firstname", 1)]&page=5 + $ curl -i http://myapi.com/people?where={"lastname": "Doe"}&sort=[("firstname", 1)]&page=5 HTTP/1.1 200 OK Pagination can be disabled. Please note that, for clarity, the above example is @@ -498,7 +496,7 @@ edits) are in JSON format. .. code-block:: console - $ curl -H "Accept: application/xml" -i http://eve-demo.herokuapp.com + $ curl -H "Accept: application/xml" -i http://myapi.com HTTP/1.1 200 OK Content-Type: application/xml; charset=utf-8 ... @@ -534,14 +532,14 @@ conditional requests by using the ``If-Modified-Since`` header: .. code-block:: console - $ curl -H "If-Modified-Since: Wed, 05 Dec 2012 09:53:07 GMT" -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c + $ curl -H "If-Modified-Since: Wed, 05 Dec 2012 09:53:07 GMT" -i http://myapi.com/people/521d6840c437dc0002d1203c HTTP/1.1 200 OK or the ``If-None-Match`` header: .. code-block:: console - $ curl -H "If-None-Match: 1234567890123456789012345678901234567890" -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c + $ curl -H "If-None-Match: 1234567890123456789012345678901234567890" -i http://myapi.com/people/521d6840c437dc0002d1203c HTTP/1.1 200 OK @@ -560,7 +558,7 @@ Consider the following workflow: .. code-block:: console - $ curl -H "Content-Type: application/json" -X PATCH -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c -d '{"firstname": "ronald"}' + $ curl -H "Content-Type: application/json" -X PATCH -i http://myapi.com/people/521d6840c437dc0002d1203c -d '{"firstname": "ronald"}' HTTP/1.1 428 PRECONDITION REQUIRED We attempted an edit (``PATCH``), but we did not provide an ``ETag`` for the @@ -568,7 +566,7 @@ item so we got a ``428 PRECONDITION REQUIRED`` back. Let's try again: .. code-block:: console - $ curl -H "If-Match: 1234567890123456789012345678901234567890" -H "Content-Type: application/json" -X PATCH -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c -d '{"firstname": "ronald"}' + $ curl -H "If-Match: 1234567890123456789012345678901234567890" -H "Content-Type: application/json" -X PATCH -i http://myapi.com/people/521d6840c437dc0002d1203c -d '{"firstname": "ronald"}' HTTP/1.1 412 PRECONDITION FAILED What went wrong this time? We provided the mandatory ``If-Match`` header, but @@ -577,7 +575,7 @@ currently stored on the server, so we got a ``412 PRECONDITION FAILED``. Again! .. code-block:: console - $ curl -H "If-Match: 80b81f314712932a4d4ea75ab0b76a4eea613012" -H "Content-Type: application/json" -X PATCH -i http://eve-demo.herokuapp.com/people/50adfa4038345b1049c88a37 -d '{"firstname": "ronald"}' + $ curl -H "If-Match: 80b81f314712932a4d4ea75ab0b76a4eea613012" -H "Content-Type: application/json" -X PATCH -i http://myapi.com/people/50adfa4038345b1049c88a37 -d '{"firstname": "ronald"}' HTTP/1.1 200 OK Finally! And the response payload looks something like this: @@ -620,7 +618,7 @@ A client may submit a single document for insertion: .. code-block:: console - $ curl -d '{"firstname": "barack", "lastname": "obama"}' -H 'Content-Type: application/json' http://eve-demo.herokuapp.com/people + $ curl -d '{"firstname": "barack", "lastname": "obama"}' -H 'Content-Type: application/json' http://myapi.com/people HTTP/1.1 201 OK In this case the response payload will just contain the relevant document @@ -646,7 +644,7 @@ documents in a JSON list: .. code-block:: console - $ curl -d '[{"firstname": "barack", "lastname": "obama"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://eve-demo.herokuapp.com/people + $ curl -d '[{"firstname": "barack", "lastname": "obama"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://myapi.com/people HTTP/1.1 201 OK The response will be a list itself, with the state of each document: @@ -691,7 +689,7 @@ will only be updated if validation passes. .. code-block:: console - $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://eve-demo.herokuapp.com/people + $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://myapi.com/people HTTP/1.1 201 OK The response will contain a success/error state for each item provided in the @@ -825,7 +823,7 @@ You can set global and individual cache-control directives for each resource. .. code-block:: console - $ curl -i http://eve-demo.herokuapp.com + $ curl -i http://myapi HTTP/1.1 200 OK Content-Type: application/json Content-Length: 131 @@ -980,7 +978,7 @@ where the client dictates which fields should be returned by the API. .. code-block:: console - $ curl -i -G http://eve-demo.herokuapp.com/people --data-urlencode 'projection={"lastname": 1, "born": 1}' + $ curl -i -G http://myapi.com/people --data-urlencode 'projection={"lastname": 1, "born": 1}' HTTP/1.1 200 OK The query above will only return *lastname* and *born* out of all the fields @@ -988,7 +986,7 @@ available in the 'people' resource. You can also exclude fields: .. code-block:: console - $ curl -i -G http://eve-demo.herokuapp.com/people --data-urlencode 'projection={"born": 0}' + $ curl -i -G http://myapi.com/people --data-urlencode 'projection={"born": 0}' HTTP/1.1 200 OK The above will return all fields but *born*. Please note that key fields such diff --git a/docs/index.rst b/docs/index.rst index 9e71ad8b4..b6697bc5b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,18 +75,6 @@ You can support Eve development by pledging on GitHub, Patreon, or PayPal. - `Become a Backer on Patreon `_ - `Donate via PayPal `_ (one time) -.. _demo: - -Live demo ---------- -Check out the `live demo`_. If using a browser you will get XML back. For JSON -in the browser, you might want to install Postman_ or similar extension and -then set the ``Accept`` request header to ``application/json``. If you are -a CLI user (and you should), ``curl`` is your friend. The `source code`_ will -show you how easy it is to run an API with Eve. You will also find `usage -examples`_ for all common use cases (GET, POST, PATCH, DELETE and more). There -is also a simple `client app`_ available. - .. toctree:: :hidden: @@ -115,11 +103,6 @@ is also a simple `client app`_ available. .. _python-eve.org: http://python-eve.org -.. _`Eve Demo instructions`: http://github.com/pyeve/eve-demo#readme -.. _`live demo`: https://eve-demo.herokuapp.com/people -.. _`source code`: https://github.com/pyeve/eve-demo -.. _`usage examples`: https://github.com/pyeve/eve-demo#readme -.. _`client app`: https://github.com/pyeve/eve-demo-client .. _Postman: https://www.getpostman.com .. _Flask: http://flask.pocoo.org/ .. _eve-sqlalchemy: https://github.com/RedTurtle/eve-sqlalchemy diff --git a/docs/validation.rst b/docs/validation.rst index c6f2f7722..aeb074a55 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -9,7 +9,7 @@ will only be updated if validation passes. .. code-block:: console - $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://eve-demo.herokuapp.com/people + $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://myapi/people HTTP/1.1 201 OK The response will contain a success/error state for each item provided in the @@ -169,7 +169,7 @@ a payload like this will be accepted: .. code-block:: console - $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "bill", "age":70}]' -H 'Content-Type: application/json' http://eve-demo.herokuapp.com/people + $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "bill", "age":70}]' -H 'Content-Type: application/json' http://myapi/people HTTP/1.1 201 OK .. admonition:: Please note From b5226919a9ba10f40000c449ecbece97e4bb6e5e Mon Sep 17 00:00:00 2001 From: JeffZhang Date: Thu, 26 Nov 2020 11:32:45 +0800 Subject: [PATCH 663/821] fix #1423 --- eve/versioning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eve/versioning.py b/eve/versioning.py index 8eff8cba4..08cb0c712 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -154,8 +154,7 @@ def insert_versioning_documents(resource, documents): versioned_documents.append(ver_doc) # bulk insert - source = resource_def["datasource"]["source"] - versionable_resource_name = source + app.config["VERSIONS"] + versionable_resource_name = resource + app.config["VERSIONS"] app.data.insert(versionable_resource_name, versioned_documents) From 8aefa6faaa8dc327b540c3a1f7b5f51e42559d42 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 28 Nov 2020 09:00:29 +0100 Subject: [PATCH 664/821] changelog for #1424 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d75db4425..120979dd6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Disable MD5 support in GridFS, as it is deprecated (`#1410`_). +- Versioning: support for dynamic datasources (`#1423`) +- Disable MD5 support in GridFS, as it is deprecated (`#1410`_) - Demo application has been terminated by Heroky; dropped any reference to it. +.. _`#1423`: https://github.com/pyeve/eve/issues/1423 .. _`#1410`: https://github.com/pyeve/eve/issues/1410 Version 1.1.4 From 2fd6854c61c15adc9b2036c52888500044eb5925 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 28 Nov 2020 09:00:39 +0100 Subject: [PATCH 665/821] Jeff Zhang --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b7098a1a3..e7feb8d7e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -81,6 +81,7 @@ Patches and Contributions - Javier Gonel - Javier Jiménez - Jean Boussier +- Jeff Zhang - Jen Montes - Jeremy Solbrig - Joakim Uddholm From 1250884262cc63116299e279926a2ad525812504 Mon Sep 17 00:00:00 2001 From: Rahul Salgare Date: Mon, 30 Nov 2020 14:23:56 +0530 Subject: [PATCH 666/821] corrected variable name --- docs/features.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index e722b1d7b..df43ea229 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1507,7 +1507,7 @@ Example: .. code-block:: pycon >>> def before_insert(resource_name, items): - ... print('About to store items to "%s" ' % resource) + ... print('About to store items to "%s" ' % resource_name) >>> def after_insert_contacts(items): ... print('About to store contacts') From a2762786fbc15ee84d9ebaf0adfb781450c7c597 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 5 Dec 2020 11:23:31 +0100 Subject: [PATCH 667/821] changelog for #1426 --- CHANGES.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 120979dd6..06dc5f181 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,10 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Versioning: support for dynamic datasources (`#1423`) +- Documentation: corrected variable name (`#1426`_) +- Versioning: support for dynamic datasources (`#1423`_) - Disable MD5 support in GridFS, as it is deprecated (`#1410`_) -- Demo application has been terminated by Heroky; dropped any reference to it. +- Demo application has been terminated by Heroku. Dropped any reference to it. +.. _`#1426`: https://github.com/pyeve/eve/pull/1426 .. _`#1423`: https://github.com/pyeve/eve/issues/1423 .. _`#1410`: https://github.com/pyeve/eve/issues/1410 From 10437e8368fb2601d2ac3fec9d8ff0e067004aab Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 5 Dec 2020 11:24:06 +0100 Subject: [PATCH 668/821] Rahul Salgare --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e7feb8d7e..773828881 100644 --- a/AUTHORS +++ b/AUTHORS @@ -157,6 +157,7 @@ Patches and Contributions - Prajjwal Nijhara - Prayag Verma - Qiang Zhang +- Rahul Salgare - Ralph Smith - Raychee - Robert Wlodarczyk From aae415acc1393e654be5c203f132d3dc14c1c43c Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Fri, 22 Jan 2021 17:29:02 +0100 Subject: [PATCH 669/821] Fix unique field query construction when field is nested Add test for different unique field locations --- eve/io/mongo/validation.py | 30 +++++++++------ eve/tests/methods/post.py | 78 +++++++++++++++++++++++++++++++++----- eve/tests/test_settings.py | 41 ++++++++++++++++++-- 3 files changed, 125 insertions(+), 24 deletions(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index ad18ec1eb..0ef9f08b5 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -94,19 +94,27 @@ def _is_value_unique(self, unique, field, value, query): .. versionadded:: 0.6 """ if unique: - schema = self.schema - attribute_path = self.document_path + (field,) - temp_path = [attribute_path[0]] - for i, path in enumerate(attribute_path[:-1]): - schema = schema[path] - if schema["type"] != "list": - temp_path.append(attribute_path[i + 1]) - - final_path = ".".join(temp_path) - - query[final_path] = value + # In order to create the right query to check for unique values + # We need to obtain the schema path for the current field + # excluding any list fields in between. + schema = self.root_schema + document_field_path = list(self.document_path) + [field] + field_schema_path = [] + + while document_field_path: + current_schema_path_type = schema.get("type") + path = document_field_path.pop(0) + if current_schema_path_type == "dict": + schema = schema["schema"][path] + field_schema_path.append(path) + elif schema.get("type") == "list": + schema = schema["schema"] + else: + schema = schema[path] + field_schema_path.append(path) + query[".".join(field_schema_path)] = value resource_config = config.DOMAIN[self.resource] # exclude soft deleted documents if applicable diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 8844c8527..b2cbfc956 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1020,17 +1020,77 @@ def test_unique_within_resource_value_different_resources(self): self.assert201(status) def test_unique_within_resource_in_resource_without_filter(self): - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value"} - ) + def make_payload(unique_value): + return {"unique_within_resource_attribute": unique_value} + + r, status = self.post("test_unique", data=make_payload("unique_value")) self.assert201(status) - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value"} - ) + r, status = self.post("test_unique", data=make_payload("unique_value")) self.assert422(status) - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value 2"} - ) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_root_attribute(self): + def make_payload(unique_value): + return {"unique_attribute": unique_value} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_dict_attribute(self): + def make_payload(unique_value): + return {"unique_in_dict_attribute": {"unique_attribute": unique_value}} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_list_attribute(self): + def make_payload(unique_value): + return {"unique_in_list_attribute": [{"unique_attribute": unique_value}]} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_deep_dict_attribute(self): + def make_payload(unique_value): + return { + "unique_in_deep_dict_attribute": { + "dict_attribute": {"unique_attribute": unique_value} + } + } + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_deep_list_attribute(self): + def make_payload(unique_value): + return { + "unique_in_deep_list_attribute": { + "list_attribute": [{"unique_attribute": unique_value}] + } + } + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) self.assert201(status) def perform_post(self, data, valid_items=[0]): diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index ca7c249eb..84d462cc4 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -302,6 +302,40 @@ "datasource": {"source": "test_unique"}, "schema": { "unique_attribute": {"type": "string", "unique": True}, + "unique_in_dict_attribute": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + }, + "unique_in_list_attribute": { + "type": "list", + "schema": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + }, + }, + "unique_in_deep_dict_attribute": { + "type": "dict", + "schema": { + "dict_attribute": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + } + }, + }, + "unique_in_deep_list_attribute": { + "type": "dict", + "schema": { + "list_attribute": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "unique_attribute": {"type": "string", "unique": True} + }, + }, + } + }, + }, "unique_within_resource_attribute": { "type": "string", "unique_within_resource": True, @@ -309,6 +343,8 @@ }, } +test_unique_nested = {"datasource": {"source": "test_unique_nested"}, "schema": {}} + credit_rules = { "allow_unknown": True, "schema": { @@ -331,10 +367,7 @@ brands = { "item_title": "brand", - "schema": { - "name": {"type": "string"}, - "address": "string", - }, + "schema": {"name": {"type": "string"}, "address": "string"}, } components = { From 90f4b3d981baf9894773ad814888c544bd250299 Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Sat, 23 Jan 2021 11:26:01 +0100 Subject: [PATCH 670/821] Rerun CI From c44433fcce249d0f0d9a0763bc70dd052b2e99e2 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 25 Jan 2021 10:05:43 +0100 Subject: [PATCH 671/821] changelog for #1435 --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 06dc5f181..b75c52bf8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,11 +6,15 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +Fixed +~~~~~ +- Nested unique field validation still don't work (`#1436`_) - Documentation: corrected variable name (`#1426`_) - Versioning: support for dynamic datasources (`#1423`_) - Disable MD5 support in GridFS, as it is deprecated (`#1410`_) - Demo application has been terminated by Heroku. Dropped any reference to it. +.. _`#1436`: https://github.com/pyeve/eve/issues/1436 .. _`#1426`: https://github.com/pyeve/eve/pull/1426 .. _`#1423`: https://github.com/pyeve/eve/issues/1423 .. _`#1410`: https://github.com/pyeve/eve/issues/1410 From 6a9e5b36bbcd0d2e020197386c40dd4bd6b294bc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 25 Jan 2021 10:12:54 +0100 Subject: [PATCH 672/821] fix broken link --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b75c52bf8..0f547382a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,13 +8,13 @@ In Development Fixed ~~~~~ -- Nested unique field validation still don't work (`#1436`_) +- Nested unique field validation still don't work (`#1435`_) - Documentation: corrected variable name (`#1426`_) - Versioning: support for dynamic datasources (`#1423`_) - Disable MD5 support in GridFS, as it is deprecated (`#1410`_) - Demo application has been terminated by Heroku. Dropped any reference to it. -.. _`#1436`: https://github.com/pyeve/eve/issues/1436 +.. _`#1435`: https://github.com/pyeve/eve/issues/1435 .. _`#1426`: https://github.com/pyeve/eve/pull/1426 .. _`#1423`: https://github.com/pyeve/eve/issues/1423 .. _`#1410`: https://github.com/pyeve/eve/issues/1410 From 1fde8b9ba677ccc298e067f5b4e2fdb7e07220be Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 25 Jan 2021 10:19:13 +0100 Subject: [PATCH 673/821] bump version to 1.1.5 --- CHANGES.rst | 8 ++++++++ eve/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0f547382a..bcf499f29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,16 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version 1.1.5 +------------- + +Released on January 25, 2021. + Fixed ~~~~~ + - Nested unique field validation still don't work (`#1435`_) - Documentation: corrected variable name (`#1426`_) - Versioning: support for dynamic datasources (`#1423`_) diff --git a/eve/__init__.py b/eve/__init__.py index fb307c677..146180b65 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.5.dev0" +__version__ = "1.1.5" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 2b3f8a6e612f030aec50877109c7fef70b93a917 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 25 Jan 2021 10:19:50 +0100 Subject: [PATCH 674/821] bump version to 1.1.6.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 146180b65..1e4b4f315 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.5" +__version__ = "1.1.6.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 3814fdc6154985eabd4d2044e248dac8d4b562c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Thu, 28 Jan 2021 20:16:54 +0100 Subject: [PATCH 675/821] Add Python 3.9 support --- .travis.yml | 2 ++ AUTHORS | 1 + CHANGES.rst | 2 +- setup.py | 1 + tox.ini | 2 +- 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7569496a..2129dc5eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,5 +24,7 @@ matrix: python: "3.7" - env: TOXENV=py38 python: "3.8" + - env: TOXENV=py39 + python: "3.9" - env: TOXENV=pypy3 python: "pypy3.5-6.0" diff --git a/AUTHORS b/AUTHORS index 773828881..294e85197 100644 --- a/AUTHORS +++ b/AUTHORS @@ -130,6 +130,7 @@ Patches and Contributions - Mayur Dhamanwala - Michael Maxwell - Mikael Berg +- Miroslav Šedivý - Moritz Schneider - Moritz Schneider - Mugur Rus diff --git a/CHANGES.rst b/CHANGES.rst index bcf499f29..39efa5485 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Added Python 3.9 support Version 1.1.5 ------------- diff --git a/setup.py b/setup.py index e6b7ac007..26b8163d6 100755 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", diff --git a/tox.ini b/tox.ini index 62f54eb78..d7c89176b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py35,py36,py37,py38,pypy3,linting +envlist=py27,py35,py36,py37,py38,py39,pypy3,linting [testenv] extras=tests From ef537ed57bbd25adc35523a5396e97cb1d93a3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:33:34 +0100 Subject: [PATCH 676/821] Drop Python 3.5 support --- .travis.yml | 2 -- CHANGES.rst | 2 +- setup.py | 3 +-- tox.ini | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2129dc5eb..aec51649d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: python: "3.7" - env: TOXENV=py27 python: "2.7" - - env: TOXENV=py35 - python: "3.5" - env: TOXENV=py36 python: "3.6" - env: TOXENV=py37 diff --git a/CHANGES.rst b/CHANGES.rst index 39efa5485..d3c00aa37 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Added Python 3.9 support +- Added Python 3.9 support and dropped Python 3.5 support Version 1.1.5 ------------- diff --git a/setup.py b/setup.py index 26b8163d6..a7eb36c4f 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ test_suite="eve.tests", install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, !=3.4.*", + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, !=3.4.*, !=3.5.*", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -59,7 +59,6 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tox.ini b/tox.ini index d7c89176b..d38ae0e7f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py35,py36,py37,py38,py39,pypy3,linting +envlist=py27,py36,py37,py38,py39,pypy3,linting [testenv] extras=tests From 78210ff9490c825795b3f0b33d46add256b0abfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:34:01 +0100 Subject: [PATCH 677/821] Update pypi to 3.6-7.3.1 in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aec51649d..ac01c20c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ matrix: - env: TOXENV=py39 python: "3.9" - env: TOXENV=pypy3 - python: "pypy3.5-6.0" + python: "pypy3.6-7.3.1" From c6a8cf6db3b277bdd70f31b844058efea3769b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sun, 7 Feb 2021 11:43:55 +0100 Subject: [PATCH 678/821] Add GitHub Actions CI --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ README.rst | 3 +++ tox.ini | 9 +++++++++ 3 files changed, 51 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e64edc26d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: [push, pull_request, workflow_dispatch] + +jobs: + tests: + name: "Python ${{ matrix.python-version }}, Mongo ${{ matrix.mongodb-version }}, Redis ${{ matrix.redis-version }} on ${{ matrix.os }}" + runs-on: "${{ matrix.os }}" + + strategy: + matrix: + python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "pypy3"] + os: ["ubuntu-latest"] + mongodb-version: ["4.0", "4.2", "4.4"] + redis-version: ["4", "5", "6"] + + steps: + - uses: "actions/checkout@v2" + - uses: "actions/setup-python@v2" + with: + python-version: "${{ matrix.python-version }}" + - uses: "supercharge/mongodb-github-action@1.3.0" + with: + mongodb-version: "${{ matrix.mongodb-version }}" + - uses: "supercharge/redis-github-action@1.2.0" + with: + redis-version: ${{ matrix.redis-version }} + - name: "Install dependencies" + run: | + set -xe + python -VV + python -m site + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade virtualenv tox tox-gh-actions + - name: "Start mongo ${{ matrix.mongodb-version }}" + run: | + mongo eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' + - name: "Run tox targets for ${{ matrix.python-version }}" + run: "python -m tox" diff --git a/README.rst b/README.rst index cf2f9623b..13e8d0c0b 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,9 @@ Eve .. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square :target: https://pypi.org/project/eve +.. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg + :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI + .. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square :target: https://travis-ci.org/pyeve/eve diff --git a/tox.ini b/tox.ini index d38ae0e7f..5e0140afa 100644 --- a/tox.ini +++ b/tox.ini @@ -15,3 +15,12 @@ commands = pre-commit run --all-files [flake8] max-line-length = 88 ignore = E401,E722,W503,F821,E501,E203 + +[gh-actions] +python = + 2.7: py27 + 3.6: py36 + 3.7: py37, linting + 3.8: py38 + 3.9: py39 + pypy3: pypy3 From 9303b68d21b08a4b4f3f8d9affb849a40ddeaad3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 20 Feb 2021 10:47:23 +0100 Subject: [PATCH 679/821] Add proper pull requests references to changelog --- CHANGES.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d3c00aa37..95c9a9a7d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Added Python 3.9 support and dropped Python 3.5 support +- Add GitHub Actions to CI (`#1439`_) +- Added Python 3.9 support (`#1437`_) +- Dropped Python 3.5 support (`#1438`_) + +.. _`#1439`: https://github.com/pyeve/eve/pull/1439 +.. _`#1438`: https://github.com/pyeve/eve/pull/1438 +.. _`#1437`: https://github.com/pyeve/eve/pull/1437 Version 1.1.5 ------------- From 838c2865c39e59822ff90317081d903828dcdc5c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 20 Feb 2021 10:49:23 +0100 Subject: [PATCH 680/821] limit the CI runs on redis and mongo. Addresses #1439 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e64edc26d..5fb65d543 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: matrix: python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "pypy3"] os: ["ubuntu-latest"] - mongodb-version: ["4.0", "4.2", "4.4"] - redis-version: ["4", "5", "6"] + mongodb-version: ["4.4"] + redis-version: ["6"] steps: - uses: "actions/checkout@v2" From 56e97821a856d9bac5e20f52ee80770178895175 Mon Sep 17 00:00:00 2001 From: Fouad CHENNOUF Date: Thu, 18 Feb 2021 18:26:47 +0100 Subject: [PATCH 681/821] do not return related fields if the field is a list and empty --- eve/methods/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 9e39df5b3..671f1fb36 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -754,7 +754,7 @@ def resolve_data_relation_links(document, resource): if "data_relation" not in field_def: continue - if field in document and document[field] is not None: + if field in document and document[field] is not None and document[field] is not []: related_links = [] # Make the code DRY for list of linked relation and single linked relation From 1a0f7ce2bd548c986c791081c3bb112e155e4adb Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 20 Feb 2021 10:53:40 +0100 Subject: [PATCH 682/821] linting fix --- eve/methods/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 671f1fb36..39234a2c9 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -754,7 +754,11 @@ def resolve_data_relation_links(document, resource): if "data_relation" not in field_def: continue - if field in document and document[field] is not None and document[field] is not []: + if ( + field in document + and document[field] is not None + and document[field] is not [] + ): related_links = [] # Make the code DRY for list of linked relation and single linked relation From dc80d4eedf5110d42b4d4b792df69a73d709e523 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 20 Feb 2021 11:02:32 +0100 Subject: [PATCH 683/821] changelog for #1441 --- CHANGES.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 95c9a9a7d..cc40b2cdf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,19 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Add GitHub Actions to CI (`#1439`_) +Fixed +~~~~~ + +- Do not return related fields if field is a empty list (`#1441`_) + +.. _`#1441`: https://github.com/pyeve/eve/pull/1441 + +New +~~~ + - Added Python 3.9 support (`#1437`_) - Dropped Python 3.5 support (`#1438`_) +- Add GitHub Actions to CI (`#1439`_) .. _`#1439`: https://github.com/pyeve/eve/pull/1439 .. _`#1438`: https://github.com/pyeve/eve/pull/1438 From 33b8c29cf101d7ad9c012c5ce5a60ce8434e09d8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 20 Feb 2021 11:06:48 +0100 Subject: [PATCH 684/821] Fouad Chennou --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 294e85197..a0d4fed0c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -58,6 +58,7 @@ Patches and Contributions - Ewan Higgs - Felix Peppert - Florian Rathgeber +- Fouad Chennou - Francisco Corrales Morales - Garrin Kimmell - George Lestaris @@ -132,7 +133,6 @@ Patches and Contributions - Mikael Berg - Miroslav Šedivý - Moritz Schneider -- Moritz Schneider - Mugur Rus - Nathan Reynolds - Niall Donegan From ca6e79c1a656c50609f10e1c78744ce4cf084220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:38:53 +0100 Subject: [PATCH 685/821] Add GHA CI badge to docs --- docs/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index b6697bc5b..c6874c9a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,9 @@ Version |release|. .. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square :target: https://pypi.org/project/eve +.. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg + :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI + .. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square :target: https://travis-ci.org/pyeve/eve From 85c75aa6cb7aa573e6ba24719d7bd3d8ca0de332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:39:44 +0100 Subject: [PATCH 686/821] Drop Python 3.5 from docs --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c6874c9a0..0339e8afa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,7 +36,7 @@ Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.7, 3.5+, and PyPy. +The codebase is thoroughly tested under Python 2.7, 3.6+, and PyPy. .. note:: The use of **Python 3** is *highly* preferred over Python 2. Consider upgrading your applications and infrastructure if you find yourself *still* using Python 2 in production today. From c786460455ba443de70dd876bf375e9dd7ca1aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:40:58 +0100 Subject: [PATCH 687/821] Drop Travis --- .travis.yml | 28 ---------------------------- README.rst | 3 --- docs/index.rst | 3 --- 3 files changed, 34 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac01c20c1..000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -dist: xenial -language: python -cache: pip -services: - - mongodb - - redis-server -before_script: - - sleep 15 - - mongo eve_test --eval 'db.createUser({user:"test_user",pwd:"test_pw",roles:["readWrite"]});' -install: travis_retry pip install tox-travis -script: tox --recreate - -matrix: - include: - - env: TOXENV=linting - python: "3.7" - - env: TOXENV=py27 - python: "2.7" - - env: TOXENV=py36 - python: "3.6" - - env: TOXENV=py37 - python: "3.7" - - env: TOXENV=py38 - python: "3.8" - - env: TOXENV=py39 - python: "3.9" - - env: TOXENV=pypy3 - python: "pypy3.6-7.3.1" diff --git a/README.rst b/README.rst index 13e8d0c0b..a6eee1f0c 100644 --- a/README.rst +++ b/README.rst @@ -6,9 +6,6 @@ Eve .. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI -.. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square - :target: https://travis-ci.org/pyeve/eve - .. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square :target: https://pypi.org/project/eve diff --git a/docs/index.rst b/docs/index.rst index 0339e8afa..95c2f7f7f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,9 +14,6 @@ Version |release|. .. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI -.. image:: https://img.shields.io/travis/pyeve/eve.svg?branch=master&style=flat-square - :target: https://travis-ci.org/pyeve/eve - .. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square :target: https://pypi.org/project/eve From 1f2e578b2b1d461300ddbf5c431b9dbfed7d147b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 27 Feb 2021 09:30:34 +0100 Subject: [PATCH 688/821] Changelog for #1444 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index cc40b2cdf..7e9106afb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,8 +18,9 @@ New - Added Python 3.9 support (`#1437`_) - Dropped Python 3.5 support (`#1438`_) -- Add GitHub Actions to CI (`#1439`_) +- Swtich from Travis CI to GitHub Actions (`#1439`_, `#1444`_) +.. _`#1444`: https://github.com/pyeve/eve/pull/1444 .. _`#1439`: https://github.com/pyeve/eve/pull/1439 .. _`#1438`: https://github.com/pyeve/eve/pull/1438 .. _`#1437`: https://github.com/pyeve/eve/pull/1437 From 833f6c3f5454168c1efb7edf7dfbd96335779b5f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 27 Feb 2021 09:31:22 +0100 Subject: [PATCH 689/821] docs: drop reference to demo app --- docs/quickstart.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 950f81543..4dceff3ba 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -294,11 +294,5 @@ endpoint: Cache directives and item title match our new settings. See :doc:`features` for a complete list of features available and more usage examples. -.. note:: - All examples and code snippets are from the :ref:`demo`, which is a fully - functional API that you can use to experiment on your own, either on the - live instance or locally (you can use the sample client app to populate - and/or reset the database). - .. _`installed`: http://docs.mongodb.org/manual/installation/ .. _running: http://docs.mongodb.org/manual/tutorial/manage-mongodb-processes/ From e6f66d927e039399b2e6c90845f53648a6b2069c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:38:33 +0100 Subject: [PATCH 690/821] Use Python3 division of integers --- eve/methods/get.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index 04f9df61e..37dc0b1e0 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -10,6 +10,8 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +from __future__ import division + import math import copy @@ -638,7 +640,7 @@ def _pagination_links(resource, req, document_count, document_id=None): } if document_count: - last_page = int(math.ceil(document_count / float(req.max_results))) + last_page = int(math.ceil(document_count / req.max_results)) q = querydef( req.max_results, req.where, From b26fd5911be1bdeadf8253b5423487bdd4ebd5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:38:50 +0100 Subject: [PATCH 691/821] Simplify multiple isinstance --- eve/io/mongo/geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index 8c6cee521..7b1853663 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -26,7 +26,7 @@ def _correct_position(self, position): return ( isinstance(position, list) and len(position) > 1 - and all(isinstance(pos, int) or isinstance(pos, float) for pos in position) + and all(isinstance(pos, (int, float)) for pos in position) ) From 640b70b1ea30f0637b21553d5b01fa413e552443 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 6 Mar 2021 09:48:48 +0100 Subject: [PATCH 692/821] changelog for #1445 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7e9106afb..26be62047 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,7 +10,9 @@ Fixed ~~~~~ - Do not return related fields if field is a empty list (`#1441`_) +- Prepare for Python 3 switch (`#1445`_) +.. _`#1445`: https://github.com/pyeve/eve/pull/1445 .. _`#1441`: https://github.com/pyeve/eve/pull/1441 New From 97c1d7096ef59265b507364066b089cc42bd40e5 Mon Sep 17 00:00:00 2001 From: alexmisk Date: Wed, 10 Mar 2021 23:31:59 +0300 Subject: [PATCH 693/821] fix fork link in contributing info --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9194dff8d..5bd5a6cf3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -87,7 +87,7 @@ First time setup .. _latest version of git: https://git-scm.com/downloads .. _username: https://help.github.com/articles/setting-your-username-in-git/ .. _email: https://help.github.com/articles/setting-your-email-in-git/ -.. _Fork: https://github.com/pallets/flask/fork +.. _Fork: https://github.com/pyeve/eve/fork .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork Start coding From 9ccea9d5af78a29428e5026e7de650191c7f0a9a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 14 Mar 2021 17:46:58 +0100 Subject: [PATCH 694/821] Changelog for #1447 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 26be62047..05e6fab1b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,9 +9,11 @@ In Development Fixed ~~~~~ +- Fix fork link in contributing info (`#1447`_) - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1447`: https://github.com/pyeve/eve/pull/1447 .. _`#1445`: https://github.com/pyeve/eve/pull/1445 .. _`#1441`: https://github.com/pyeve/eve/pull/1441 From c00e16fe5093ea76f2f95d5116fbdb9dd81fb4bb Mon Sep 17 00:00:00 2001 From: Adrian_Cin Date: Thu, 1 Apr 2021 13:33:24 +0700 Subject: [PATCH 695/821] Fix typo in config --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 79763b49d..78cdb1f21 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -749,7 +749,7 @@ uppercase. altogether. Defaults to ``[400, 401, 403, 404, 405, 406, 409, 410, 412, 422, 428]`` -``VALIDATION_ERROR_AS_STRING`` If ``True`` even single field errors will +``VALIDATION_ERROR_AS_LIST`` If ``True`` even single field errors will be returned in a list. By default single field errors are returned as strings while multiple field errors are bundled in a From 9e72754dbc2d321729a40fe62fa7e90e73b23c53 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 6 Nov 2021 14:22:07 +0100 Subject: [PATCH 696/821] Adrian Cin --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index a0d4fed0c..8424c88dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Patches and Contributions - Aayush Sarva - Adam Walsh +- Adrian Cin - Alberto Marin - Alex Misk - Alexander Dietmüller From 30a7cc80e9a1b40665cd7e7ef93a470ae692da02 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 10 Dec 2021 11:01:16 +0100 Subject: [PATCH 697/821] pin pymongo version in dependencies. Closes #1461 --- CHANGES.rst | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 05e6fab1b..45dba9280 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,10 +9,12 @@ In Development Fixed ~~~~~ +- Pin pymongo version in dependencies (`#1461`_) - Fix fork link in contributing info (`#1447`_) - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1447`: https://github.com/pyeve/eve/pull/1447 .. _`#1445`: https://github.com/pyeve/eve/pull/1445 .. _`#1441`: https://github.com/pyeve/eve/pull/1441 diff --git a/setup.py b/setup.py index a7eb36c4f..c18a0166e 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", "flask", - "pymongo>=3.7", + "pymongo>=3.7,<4.0", "simplejson>=3.3.0,<4.0", ] From d7c06459fbe2142131a390bfb4a2060ea3a6372a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 17:05:15 +0100 Subject: [PATCH 698/821] fix: PyMongo 3.12+ supports keys that include dotted fields Closes #1466 --- CHANGES.rst | 2 ++ eve/tests/methods/post.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 45dba9280..5778239c0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,11 +9,13 @@ In Development Fixed ~~~~~ +- PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) - Pin pymongo version in dependencies (`#1461`_) - Fix fork link in contributing info (`#1447`_) - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1466`: https://github.com/pyeve/eve/issues/1466 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1447`: https://github.com/pyeve/eve/pull/1447 .. _`#1445`: https://github.com/pyeve/eve/pull/1445 diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index b2cbfc956..ce6f070aa 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -993,14 +993,13 @@ def test_post_dont_normalize_dotted_fields(self): "test", {"normalize_dotted_fields": False, "schema": {"a_dict": {"type": "dict"}}}, ) + self.app.config["BANDWIDTH_SAVER"] = False data = {"a_dict": {"dotted.field": True}} headers = [("Content-Type", "application/json")] resp = self.test_client.post("test/", data=json.dumps(data), headers=headers) _, status = self.parse_response(resp) - # mongo returns bson.errors.InvalidDocument: - # key 'dotted.fields' must not contain '.' - self.assertEqual(500, status) + self.assertTrue(json.loads(resp.data)["a_dict"]["dotted.field"]) def test_post_projection_is_honored(self): data = {"ref": "1234567890123456789054321", "aninteger": 100} From 9cce41df035f4e4a0a4ffab916f9793d78addb71 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 17:19:36 +0100 Subject: [PATCH 699/821] tox: use Python 3.9 as base python; perform lints checks on py39 --- CHANGES.rst | 4 +++- tox.ini | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5778239c0..c93151934 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,7 +26,9 @@ New - Added Python 3.9 support (`#1437`_) - Dropped Python 3.5 support (`#1438`_) -- Swtich from Travis CI to GitHub Actions (`#1439`_, `#1444`_) +- Switch from Travis CI to GitHub Actions (`#1439`_, `#1444`_) +- Use Python 3.9 as base python for tox. +- Perform tox linting checks on Python 3.9. .. _`#1444`: https://github.com/pyeve/eve/pull/1444 .. _`#1439`: https://github.com/pyeve/eve/pull/1439 diff --git a/tox.ini b/tox.ini index 5e0140afa..453978b33 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands=py.test eve {posargs} [testenv:linting] skipsdist = True usedevelop = True -basepython = python3.7 +basepython = python3.9 deps = pre-commit commands = pre-commit run --all-files @@ -20,7 +20,7 @@ ignore = E401,E722,W503,F821,E501,E203 python = 2.7: py27 3.6: py36 - 3.7: py37, linting + 3.7: py37 3.8: py38 - 3.9: py39 + 3.9: py39, linting pypy3: pypy3 From fa2c759955687426ca92eea802d27bc529a3e829 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 17:30:24 +0100 Subject: [PATCH 700/821] Revert "tox: use Python 3.9 as base python; perform lints checks on py39" This reverts commit 9cce41df035f4e4a0a4ffab916f9793d78addb71. --- CHANGES.rst | 4 +--- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c93151934..5778239c0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,9 +26,7 @@ New - Added Python 3.9 support (`#1437`_) - Dropped Python 3.5 support (`#1438`_) -- Switch from Travis CI to GitHub Actions (`#1439`_, `#1444`_) -- Use Python 3.9 as base python for tox. -- Perform tox linting checks on Python 3.9. +- Swtich from Travis CI to GitHub Actions (`#1439`_, `#1444`_) .. _`#1444`: https://github.com/pyeve/eve/pull/1444 .. _`#1439`: https://github.com/pyeve/eve/pull/1439 diff --git a/tox.ini b/tox.ini index 453978b33..5e0140afa 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands=py.test eve {posargs} [testenv:linting] skipsdist = True usedevelop = True -basepython = python3.9 +basepython = python3.7 deps = pre-commit commands = pre-commit run --all-files @@ -20,7 +20,7 @@ ignore = E401,E722,W503,F821,E501,E203 python = 2.7: py27 3.6: py36 - 3.7: py37 + 3.7: py37, linting 3.8: py38 - 3.9: py39, linting + 3.9: py39 pypy3: pypy3 From 2c31f77e65db48e53523b10d54c8703c45249312 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 17:42:29 +0100 Subject: [PATCH 701/821] linting fix --- docs/conf.py | 12 ++++++------ eve/exceptions.py | 2 +- eve/flaskapp.py | 2 +- eve/io/mongo/validation.py | 8 ++++---- eve/methods/common.py | 2 +- eve/tests/config.py | 2 +- eve/tests/methods/get.py | 18 ++++++------------ eve/tests/methods/patch_atomic_concurrency.py | 2 +- eve/tests/methods/post.py | 2 +- eve/tests/renders.py | 4 ++-- eve/validation.py | 6 +++--- 11 files changed, 27 insertions(+), 33 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4e11326d1..b8a7368b4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,9 +43,9 @@ master_doc = "index" # General information about the project. -project = u"Eve" +project = "Eve" copyright = ( - u'%s. Python-Eve is a Nicola Iarocci Project' + '%s. Python-Eve is a Nicola Iarocci Project' % datetime.datetime.now().year ) @@ -212,7 +212,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "Eve.tex", u"Eve Documentation", u"Nicola Iarocci", "manual") + ("index", "Eve.tex", "Eve Documentation", "Nicola Iarocci", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -240,7 +240,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "eve", u"Eve Documentation", [u"Nicola Iarocci"], 1)] +man_pages = [("index", "eve", "Eve Documentation", ["Nicola Iarocci"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -255,8 +255,8 @@ ( "index", "Eve", - u"Eve Documentation", - u"Nicola Iarocci", + "Eve Documentation", + "Nicola Iarocci", "Eve", "One line description of project.", "Miscellaneous", diff --git a/eve/exceptions.py b/eve/exceptions.py index 34a561fdc..52715e5df 100644 --- a/eve/exceptions.py +++ b/eve/exceptions.py @@ -20,6 +20,6 @@ class ConfigException(Exception): class SchemaException(ConfigException): - """ Raised when errors are found in a field schema definition """ + """Raised when errors are found in a field schema definition""" pass diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 29218d9a0..a372fbaed 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -51,7 +51,7 @@ def server_version(self): class RegexConverter(BaseConverter): - """ Extend werkzeug routing by supporting regex for urls/API endpoints """ + """Extend werkzeug routing by supporting regex for urls/API endpoints""" def __init__(self, url_map, *items): super(RegexConverter, self).__init__(url_map) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 0ef9f08b5..e733a023d 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -61,11 +61,11 @@ class Validator(Validator): """ def _validate_versioned(self, unique, field, value): - """ {'type': 'boolean'} """ + """{'type': 'boolean'}""" pass def _validate_unique_to_user(self, unique, field, value): - """ {'type': 'boolean'} """ + """{'type': 'boolean'}""" auth_field, auth_value = auth_field_and_value(self.resource) # if an auth value has been set for this request, then make sure it is @@ -75,14 +75,14 @@ def _validate_unique_to_user(self, unique, field, value): self._is_value_unique(unique, field, value, query) def _validate_unique_within_resource(self, unique, field, value): - """ {'type': 'boolean'} """ + """{'type': 'boolean'}""" _, filter_, _, _ = app.data.datasource(self.resource) if filter_ is None: filter_ = {} self._is_value_unique(unique, field, value, filter_) def _validate_unique(self, unique, field, value): - """ {'type': 'boolean'} """ + """{'type': 'boolean'}""" self._is_value_unique(unique, field, value, {}) def _is_value_unique(self, unique, field, value, query): diff --git a/eve/methods/common.py b/eve/methods/common.py index 39234a2c9..9398f65bc 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1140,7 +1140,7 @@ def resolve_media_files(document, resource): def resolve_one_media(file_id, resource): - """ Get response for one media file """ + """Get response for one media file""" _file = app.media.get(file_id, resource) if _file: diff --git a/eve/tests/config.py b/eve/tests/config.py index 6b97659c3..9d5cdd8b9 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -368,7 +368,7 @@ def test_url_helpers(self): ) def test_pretty_resource_urls(self): - """ test that regexes are stripped out of urls and #466 is fixed. """ + """test that regexes are stripped out of urls and #466 is fixed.""" resource_url = self.app.config["URLS"]["peopleinvoices"] pretty_url = "users//invoices" self.assertEqual(resource_url, pretty_url) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 2fb52e963..e85f4b371 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -381,7 +381,7 @@ def test_get_projection(self): self.assertTrue(r[self.app.config["DATE_CREATED"]] != self.epoch) def test_get_static_projection(self): - """ Test that static projections are honoured """ + """Test that static projections are honoured""" response, status = self.get(self.different_resource) self.assert200(status) @@ -1101,10 +1101,7 @@ def test_get_reference_embedded_in_subdocuments(self): def test_get_reference_embedded_in_subdocuments_with_nested_dicts(self): _db = self.connection[MONGO_DBNAME] cpu_brand_name = self.random_string(10) - cpu_brand = { - "name": cpu_brand_name, - "address": self.random_string(30), - } + cpu_brand = {"name": cpu_brand_name, "address": self.random_string(30)} motherboard_brand_name = self.random_string(15) motherboard_brand = { "name": motherboard_brand_name, @@ -1179,10 +1176,7 @@ def test_get_reference_embedded_in_subdocuments_with_nested_dicts(self): ) self.assert200(result.status_code) content = json.loads(result.get_data()) - self.assertEqual( - content["components"]["cpu"]["brand"]["name"], - cpu_brand_name, - ) + self.assertEqual(content["components"]["cpu"]["brand"]["name"], cpu_brand_name) self.assertEqual( content["components"]["motherboard"]["brand"]["name"], motherboard_brand_name, @@ -1279,21 +1273,21 @@ def test_get_idfield_doesnt_exist(self): self.assert200(status) def test_get_invalid_idfield_cors(self): - """ test that #381 is fixed. """ + """test that #381 is fixed.""" request = "/%s/badid" % self.known_resource self.app.config["X_DOMAINS"] = "*" r = self.test_client.get(request, headers=[("Origin", "test.com")]) self.assert404(r.status_code) def test_get_invalid_where_syntax(self): - """ test that 'where' syntax with unknown '$' operator returns 400. """ + """test that 'where' syntax with unknown '$' operator returns 400.""" response, status = self.get( self.known_resource, '?where={"field": {"$foo": "bar"}}' ) self.assert400(status) def test_get_invalid_sort_syntax(self): - """ test that invalid sort syntax returns a 400 """ + """test that invalid sort syntax returns a 400""" response, status = self.get(self.known_resource, '?sort=[("prog":1)]') self.assert400(status) response, status = self.get(self.known_resource, '?sort="firstname"') diff --git a/eve/tests/methods/patch_atomic_concurrency.py b/eve/tests/methods/patch_atomic_concurrency.py index 2b3fac82e..7943239c3 100644 --- a/eve/tests/methods/patch_atomic_concurrency.py +++ b/eve/tests/methods/patch_atomic_concurrency.py @@ -63,7 +63,7 @@ def test_etag_changed_after_get_document(self): self.assertEqual(status, 412) def tearDown(self): - """ Remove patch of eve.methods.patch.get_document """ + """Remove patch of eve.methods.patch.get_document""" sys.modules["eve.methods.patch"].get_document = self.original_get_document return super(TestPatchAtomicConcurrent, self).tearDown() diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index ce6f070aa..9f76b695c 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -837,7 +837,7 @@ def test_post_readonly_in_dict(self): self.assertValidationErrorStatus(status) def test_post_valueschema_dict(self): - """ make sure Cerberus#48 is fixed """ + """make sure Cerberus#48 is fixed""" del self.domain["contacts"]["schema"]["ref"]["required"] r, status = self.post( self.known_resource_url, data={"valueschema_dict": {"k1": "1"}} diff --git a/eve/tests/renders.py b/eve/tests/renders.py index ad0fdacb1..c39de6737 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -360,14 +360,14 @@ def test_CORS_OPTIONS_item(self): methods = ["GET", "OPTIONS"] def test_CORS_OPTIONS_schema(self): - """ Test that CORS is also supported at SCHEMA_ENDPOINT """ + """Test that CORS is also supported at SCHEMA_ENDPOINT""" self.app.config["SCHEMA_ENDPOINT"] = "schema" self.app._init_schema_endpoint() methods = ["GET", "OPTIONS"] self.test_CORS_OPTIONS("schema", methods) def test_deprecated_renderers_supports_py27(self): - """ Make sure #1175 is fixed """ + """Make sure #1175 is fixed""" self.app.config["RENDERES"] = False try: self.app.check_deprecated_features() diff --git a/eve/validation.py b/eve/validation.py index 973e9f61c..ea62b24ea 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -65,7 +65,7 @@ def validate_replace(self, document, document_id, persisted_document=None): return super(Validator, self).validate(document) def _normalize_default(self, mapping, schema, field): - """ {'nullable': True} """ + """{'nullable': True}""" # fields with no default are of no use here if "default" not in schema[field]: @@ -97,7 +97,7 @@ def _normalize_default_setter(self, mapping, schema, field): super(Validator, self)._normalize_default_setter(mapping, schema, field) def _validate_dependencies(self, dependencies, field, value): - """ {'type': ['dict', 'hashable', 'list']} """ + """{'type': ['dict', 'hashable', 'list']}""" persisted = self._filter_persisted_fields_not_in_document(dependencies) if persisted: dcopy = copy.copy(self.document) @@ -120,7 +120,7 @@ def persisted_but_not_in_document(field): return [field for field in fields if persisted_but_not_in_document(field)] def _validate_readonly(self, read_only, field, value): - """ {'type': 'boolean'} """ + """{'type': 'boolean'}""" persisted_value = ( self.persisted_document.get(field) if self.persisted_document else None ) From 73bd04efbb88e1a62695988d95032490d059c602 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 17:52:05 +0100 Subject: [PATCH 702/821] use py39 in black's post-commit hook --- .pre-commit-config.yaml | 16 ++++------------ tox.ini | 6 +++--- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a20521c9..5ca758be1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,6 @@ repos: -- repo: https://github.com/ambv/black - rev: stable + - repo: https://github.com/psf/black + rev: 22.1.0 hooks: - - id: black - language_version: python3.7 -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.3.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: debug-statements - - id: flake8 + - id: black + language_version: python3.9 diff --git a/tox.ini b/tox.ini index 5e0140afa..f5c699426 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands=py.test eve {posargs} [testenv:linting] skipsdist = True usedevelop = True -basepython = python3.7 +basepython = python3.9 deps = pre-commit commands = pre-commit run --all-files @@ -20,7 +20,7 @@ ignore = E401,E722,W503,F821,E501,E203 python = 2.7: py27 3.6: py36 - 3.7: py37, linting + 3.7: py37 3.8: py38 - 3.9: py39 + 3.9: py39,linting pypy3: pypy3 From 72cc5d1722ed6428cafbc2fa835246d476b6c314 Mon Sep 17 00:00:00 2001 From: Raghuram Devarakonda Date: Wed, 5 Jan 2022 19:09:31 -0500 Subject: [PATCH 703/821] Fixes for couple of typos. --- docs/authentication.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index cd831a25d..3933f9940 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -58,7 +58,7 @@ to provide the correct credentials in order to consume the API: By default access is restricted to all endpoints for all HTTP verbs (methods), effectively locking down the whole API. -But what if your authorization logic is more complex, and you only want to +But what if your authentication logic is more complex, and you only want to secure some endpoints or apply different logics depending on the endpoint being consumed? You could get away with just adding logic to your authentication class, maybe with something like this: @@ -310,7 +310,7 @@ resources and/or methods to public access -see docs). def check_auth(self, token, allowed_roles, resource, method): """For the purpose of this example the implementation is as simple as possible. A 'real' token should probably contain a hash of the - username/password combo, which sould then validated against the account + username/password combo, which should then be validated against the account data stored on the DB. """ # use Eve's own db driver; no additional connections/resources are used From 8cea6b98d2d2e10880e58960d079decb974596de Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:12:02 +0100 Subject: [PATCH 704/821] Raghuram Devarakonda --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8424c88dc..d197c93cc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ Patches and Contributions - Prajjwal Nijhara - Prayag Verma - Qiang Zhang +- Raghuram Devarakonda - Rahul Salgare - Ralph Smith - Raychee From 880436e582bab1f0bf0145758f653649f9267451 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:13:46 +0100 Subject: [PATCH 705/821] changelog for #1462 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5778239c0..0e10dcb36 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,12 +9,14 @@ In Development Fixed ~~~~~ +- Documentation typos (`#1462`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) - Pin pymongo version in dependencies (`#1461`_) - Fix fork link in contributing info (`#1447`_) - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1462`: https://github.com/pyeve/eve/pull/1462 .. _`#1466`: https://github.com/pyeve/eve/issues/1466 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1447`: https://github.com/pyeve/eve/pull/1447 From 1263d427a2f33b144e336f5766e29d453085ac56 Mon Sep 17 00:00:00 2001 From: Kinuax Date: Sat, 22 Jan 2022 20:44:48 +0100 Subject: [PATCH 706/821] Update docs and tests regarding pagination of empty resources --- docs/features.rst | 4 ++-- docs/quickstart.rst | 5 +++++ eve/tests/methods/get.py | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index df43ea229..5ae4c6afb 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -100,8 +100,8 @@ These additional fields are automatically handled by the API (clients don't need to provide them when adding/editing resources). The ``_meta`` field provides pagination data and will only be there if -:ref:`Pagination` has been enabled (it is by default) and there is at least one -document being returned. The ``_links`` list provides HATEOAS_ directives. +:ref:`Pagination` has been enabled (it is by default). The ``_links`` list +provides HATEOAS_ directives. .. _subresources: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4dceff3ba..265a6e448 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -91,6 +91,11 @@ Try requesting ``people`` now: "href": "/", "title": "home" } + }, + "_meta": { + "max_results": 25, + "page": 1, + "total": 0 } } diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index e85f4b371..d3311e5ac 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -27,6 +27,8 @@ def test_get_empty_resource(self): self.assertResourceLink(links, self.empty_resource) self.assertHomeLink(links) + self.assertPagination(response, 1, 0, 25) + def test_get_max_results(self): maxr = 10 response, status = self.get(self.known_resource, "?max_results=%d" % maxr) From 5bfc3d94c1de2950553ab99899923f0eac1ee9f3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:26:48 +0100 Subject: [PATCH 707/821] kinuax --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d197c93cc..29c6e0abf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -204,6 +204,7 @@ Patches and Contributions - Xavi Cubillas - boosh - dccrazyboy +- kinuax - kreynen - mmizotin - quentinpraz From 1898f0e8fc90883940e268b7928b5a90a58c294d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:27:56 +0100 Subject: [PATCH 708/821] changelog for #1463 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0e10dcb36..58bbe6d9a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ In Development Fixed ~~~~~ +- Update docs and tests regarding pagination of empty resources (`1463`_) - Documentation typos (`#1462`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) - Pin pymongo version in dependencies (`#1461`_) @@ -16,6 +17,7 @@ Fixed - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1463`: https://github.com/pyeve/eve/pull/1463 .. _`#1462`: https://github.com/pyeve/eve/pull/1462 .. _`#1466`: https://github.com/pyeve/eve/issues/1466 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 From 9f081a2edaf23e903c01dbccfaf5b428e0dba1c1 Mon Sep 17 00:00:00 2001 From: SHaoyu Date: Thu, 19 Aug 2021 14:10:11 -0500 Subject: [PATCH 709/821] fix 500 error with empty token/bearer --- eve/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/auth.py b/eve/auth.py index 25a43da95..bb63dca0f 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -274,7 +274,7 @@ def authorized(self, allowed_roles, resource, method): if not auth and request.headers.get("Authorization"): auth = request.headers.get("Authorization").strip() if auth.lower().startswith(("token", "bearer")): - auth = auth.split(" ")[1] + auth = auth.split(" ")[1] if " " in auth else "" if auth: self.set_user_or_token(auth) From 93aef255b34221c6c6c074fbe94a00aed3595d6b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:38:36 +0100 Subject: [PATCH 710/821] changelog for #1456 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 58bbe6d9a..8862bd6e0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ In Development Fixed ~~~~~ +- Fix 500 error with empty token/bearer (`1456`_) - Update docs and tests regarding pagination of empty resources (`1463`_) - Documentation typos (`#1462`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) @@ -17,6 +18,7 @@ Fixed - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +.. _`#1456`: https://github.com/pyeve/eve/pull/156 .. _`#1463`: https://github.com/pyeve/eve/pull/1463 .. _`#1462`: https://github.com/pyeve/eve/pull/1462 .. _`#1466`: https://github.com/pyeve/eve/issues/1466 From 20ece24d2bef09221669613be4ac2688eeaebe54 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Feb 2022 18:51:10 +0100 Subject: [PATCH 711/821] fix broken changelog links --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8862bd6e0..d4f8e5043 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,8 +9,8 @@ In Development Fixed ~~~~~ -- Fix 500 error with empty token/bearer (`1456`_) -- Update docs and tests regarding pagination of empty resources (`1463`_) +- Fix 500 error with empty token/bearer (`#1456`_) +- Update docs and tests regarding pagination of empty resources (`#1463`_) - Documentation typos (`#1462`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) - Pin pymongo version in dependencies (`#1461`_) @@ -18,7 +18,7 @@ Fixed - Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) -.. _`#1456`: https://github.com/pyeve/eve/pull/156 +.. _`#1456`: https://github.com/pyeve/eve/pull/1456 .. _`#1463`: https://github.com/pyeve/eve/pull/1463 .. _`#1462`: https://github.com/pyeve/eve/pull/1462 .. _`#1466`: https://github.com/pyeve/eve/issues/1466 From b0b714e627ea92aab2e8f043903bea8f9cb5a519 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 Feb 2022 09:18:56 +0100 Subject: [PATCH 712/821] Drop Python 2 and all Pythons <3.7; Python 3.10 added to test matrix Closes #1440 --- .github/workflows/ci.yml | 36 ++++++++++++++++++---------------- CHANGES.rst | 42 +++++++++++++++++++++++++--------------- setup.py | 7 ++----- tox.ini | 11 +---------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fb65d543..64fde5662 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,36 +4,38 @@ on: [push, pull_request, workflow_dispatch] jobs: tests: - name: "Python ${{ matrix.python-version }}, Mongo ${{ matrix.mongodb-version }}, Redis ${{ matrix.redis-version }} on ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "pypy3"] - os: ["ubuntu-latest"] - mongodb-version: ["4.4"] - redis-version: ["6"] + include: + - { name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310, mongodb: '4.4', redis: '6' } + - { name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39, mongodb: '4.4', redis: '6' } + - { name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38, mongodb: '4.4', redis: '6' } + - { name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37, mongodb: '4.4', redis-version: '6' } + - { name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37, mongodb: '4.4', redis: '6' } steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: - python-version: "${{ matrix.python-version }}" - - uses: "supercharge/mongodb-github-action@1.3.0" + python-version: ${{ matrix.python }} + - uses: supercharge/mongodb-github-action@1.3.0 with: - mongodb-version: "${{ matrix.mongodb-version }}" - - uses: "supercharge/redis-github-action@1.2.0" + mongodb-version: ${{ matrix.mongodb }} + - uses: supercharge/redis-github-action@1.2.0 with: - redis-version: ${{ matrix.redis-version }} - - name: "Install dependencies" + redis-version: ${{ matrix.redis }} + - name: Install dependencies run: | set -xe python -VV python -m site python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade virtualenv tox tox-gh-actions - - name: "Start mongo ${{ matrix.mongodb-version }}" + - name: Start mongo ${{ matrix.mongodb-version }} run: | mongo eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' - - name: "Run tox targets for ${{ matrix.python-version }}" - run: "python -m tox" + - name: Run tox targets for ${{ matrix.python }} + run: tox -e ${{ matrix.tox }} diff --git a/CHANGES.rst b/CHANGES.rst index d4f8e5043..5adbcf3f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,18 +6,40 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +Breaking +~~~~~~~~ +Starting from this release, Eve supports Python 3.7 and above. + +- Drop Python 2 (`#1440`_) +- Drop Python 3.5 (`#1440`_, `#1438`_) +- Drop Python 3.6 (`#1440`_) + +.. _`#1440`: https://github.com/pyeve/eve/issues/1440 +.. _`#1438`: https://github.com/pyeve/eve/pull/1438 + +New +~~~ + +- Add Python 3.9 support (`#1437`_) +- Add Python 3.10 support (`#1440`_) + +.. _`#1444`: https://github.com/pyeve/eve/pull/1444 + Fixed ~~~~~ - Fix 500 error with empty token/bearer (`#1456`_) -- Update docs and tests regarding pagination of empty resources (`#1463`_) -- Documentation typos (`#1462`_) +- Do not return related fields if field is a empty list (`#1441`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) - Pin pymongo version in dependencies (`#1461`_) -- Fix fork link in contributing info (`#1447`_) -- Do not return related fields if field is a empty list (`#1441`_) - Prepare for Python 3 switch (`#1445`_) +- Update docs and tests regarding pagination of empty resources (`#1463`_) +- Fix fork link in contributing info (`#1447`_) +- Documentation typos (`#1462`_) +- Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) +.. _`#1439`: https://github.com/pyeve/eve/pull/1439 +.. _`#1437`: https://github.com/pyeve/eve/pull/1437 .. _`#1456`: https://github.com/pyeve/eve/pull/1456 .. _`#1463`: https://github.com/pyeve/eve/pull/1463 .. _`#1462`: https://github.com/pyeve/eve/pull/1462 @@ -27,18 +49,6 @@ Fixed .. _`#1445`: https://github.com/pyeve/eve/pull/1445 .. _`#1441`: https://github.com/pyeve/eve/pull/1441 -New -~~~ - -- Added Python 3.9 support (`#1437`_) -- Dropped Python 3.5 support (`#1438`_) -- Swtich from Travis CI to GitHub Actions (`#1439`_, `#1444`_) - -.. _`#1444`: https://github.com/pyeve/eve/pull/1444 -.. _`#1439`: https://github.com/pyeve/eve/pull/1439 -.. _`#1438`: https://github.com/pyeve/eve/pull/1438 -.. _`#1437`: https://github.com/pyeve/eve/pull/1437 - Version 1.1.5 ------------- diff --git a/setup.py b/setup.py index c18a0166e..0b80d549a 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ test_suite="eve.tests", install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, !=3.4.*, !=3.5.*", + python_requires=">=3.7", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -56,13 +56,10 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", diff --git a/tox.ini b/tox.ini index f5c699426..1f99c669f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py36,py37,py38,py39,pypy3,linting +envlist=py3{10,9,8,7},pypy3{8,7},linting [testenv] extras=tests @@ -15,12 +15,3 @@ commands = pre-commit run --all-files [flake8] max-line-length = 88 ignore = E401,E722,W503,F821,E501,E203 - -[gh-actions] -python = - 2.7: py27 - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39,linting - pypy3: pypy3 From 3fad32cdae07f236319be070e8bd0e9d7b27bfca Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 11 Feb 2022 10:39:46 +0100 Subject: [PATCH 713/821] hopefully drop Python 2 references from docs --- .gitignore | 1 + CONTRIBUTING.rst | 16 ++++++++-------- docs/index.rst | 4 +--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index ae7edef8c..77fc9f3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ _build .vscode .pytest_cache pip-wheel-metadata/ +!/.eggs/ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5bd5a6cf3..7449f3d66 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -111,27 +111,27 @@ Start coding Running the tests ~~~~~~~~~~~~~~~~~ -You should have both Python 2.7 and 3.6 available in your system. Now +You should have Python 3.7+ available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py27,py36 + $ tox -e linting,py37,py38 -This command will run tests via the "tox" tool against Python 2.7 and 3.6 and +This command will run tests via the "tox" tool against Python 3.7 and 3.8 and also perform "lint" coding-style checks. You can pass different options to ``tox``. For example, to run tests on Python -2.7 and pass options to pytest (e.g. enter pdb on failure) to pytest you can +3.10 and pass options to pytest (e.g. enter pdb on failure) to pytest you can do:: - $ tox -e py27 -- --pdb + $ tox -e py310 -- --pdb Or to only run tests in a particular test module on Python 3.6:: - $ tox -e py36 -- -k TestGet + $ tox -e py310 -- -k TestGet -Travis-CI will run the full suite when you submit your pull request. The full +CI will run the full suite when you submit your pull request. The full test suite takes a long time to run because it tests multiple combinations of -Python and dependencies. You need to have Python 2.7, 3.5, 3.6, and PyPy +Python and dependencies. You need to have Python 3.7, 3.8, 3.9, 3.10 and PyPy installed to run all of the environments. Then run:: tox diff --git a/docs/index.rst b/docs/index.rst index 95c2f7f7f..cc749fbda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,9 +33,7 @@ Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 2.7, 3.6+, and PyPy. - -.. note:: The use of **Python 3** is *highly* preferred over Python 2. Consider upgrading your applications and infrastructure if you find yourself *still* using Python 2 in production today. +The codebase is thoroughly tested under Python 3.7+, and PyPy. Eve is Simple ------------- From ba4a8e8fd3b93c79ce0832667f1a8678ee1c5d6e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 13 Feb 2022 09:45:02 +0100 Subject: [PATCH 714/821] Add support for PyMongo 4+ Closes #1461 Closes #1464 --- CHANGES.rst | 7 +++++- docs/config.rst | 4 ++-- docs/tutorials/custom_idfields.rst | 7 ++++++ eve/default_settings.py | 5 ++++- eve/io/mongo/flask_pymongo.py | 17 ++++++++------ eve/io/mongo/media.py | 2 +- eve/tests/__init__.py | 7 +++--- eve/tests/endpoints.py | 2 +- eve/tests/io/__init__.py | 4 ---- eve/tests/test_io/__init__.py | 1 + eve/tests/{io => test_io}/flask_pymongo.py | 9 +++++++- eve/tests/{io => test_io}/media.py | 0 eve/tests/{io => test_io}/mongo.py | 0 eve/tests/{io => test_io}/multi_mongo.py | 10 +-------- eve/tests/{logging.py => test_logging.py} | 0 eve/utils.py | 26 +++++++++++++++++++++- setup.py | 2 +- 17 files changed, 71 insertions(+), 32 deletions(-) delete mode 100644 eve/tests/io/__init__.py create mode 100644 eve/tests/test_io/__init__.py rename eve/tests/{io => test_io}/flask_pymongo.py (92%) rename eve/tests/{io => test_io}/media.py (100%) rename eve/tests/{io => test_io}/mongo.py (100%) rename eve/tests/{io => test_io}/multi_mongo.py (97%) rename eve/tests/{logging.py => test_logging.py} (100%) diff --git a/CHANGES.rst b/CHANGES.rst index 5adbcf3f8..9e6c084b1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,12 +22,16 @@ New - Add Python 3.9 support (`#1437`_) - Add Python 3.10 support (`#1440`_) +- ``MONGO_OPTIONS`` acquires a new ``uuidRepresentation`` setting, with ``standard`` as its default value. This is needed by PyMongo 4+ in order to seamlessly process eventual ``uuid`` values. See `PyMongo documentation`_ for details (`#1461`_, `#1464`_). + .. _`#1444`: https://github.com/pyeve/eve/pull/1444 +.. _`PyMongo documentation`: https://github.com/pyeve/eve/pull/1438 Fixed ~~~~~ +- Eve doesn't work with latest PyMongo (v4) (`#1461`_, `#1464`_) - Fix 500 error with empty token/bearer (`#1456`_) - Do not return related fields if field is a empty list (`#1441`_) - PyMongo 3.12+ supports keys that include dotted fields (`#1466`_) @@ -38,13 +42,14 @@ Fixed - Documentation typos (`#1462`_) - Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) +.. _`#1464`: https://github.com/pyeve/eve/issues/1464 +.. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1439`: https://github.com/pyeve/eve/pull/1439 .. _`#1437`: https://github.com/pyeve/eve/pull/1437 .. _`#1456`: https://github.com/pyeve/eve/pull/1456 .. _`#1463`: https://github.com/pyeve/eve/pull/1463 .. _`#1462`: https://github.com/pyeve/eve/pull/1462 .. _`#1466`: https://github.com/pyeve/eve/issues/1466 -.. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1447`: https://github.com/pyeve/eve/pull/1447 .. _`#1445`: https://github.com/pyeve/eve/pull/1445 .. _`#1441`: https://github.com/pyeve/eve/pull/1441 diff --git a/docs/config.rst b/docs/config.rst index 78cdb1f21..3baeb266a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -555,7 +555,7 @@ uppercase. ``MONGO_OPTIONS`` MongoDB keyword arguments to passed to MongoClient class ``__init__``. - Defaults to ``{'connect': True, 'tz_aware': True, 'appname': 'flask_app_name'}``. + Defaults to ``{'connect': True, 'tz_aware': True, 'appname': 'flask_app_name', 'uuidRepresentation': 'standard'}``. See `PyMongo mongo_client`_ for reference. ``MONGO_AUTH_SOURCE`` MongoDB authorization database. Defaults to ``None``. @@ -1636,4 +1636,4 @@ read access open to the public. .. _`MongoDB Aggregation Framework`: https://docs.mongodb.org/v3.0/applications/aggregation/ .. _`PyMongo aggregation defaults`: http://api.mongodb.org/python/current/api/pymongo/collection.html#pymongo.collection.Collection.aggregate .. _`PyMongo Authentication Mechanisms`: https://docs.mongodb.com/v3.0/core/authentication-mechanisms/ -.. _`PyMongo mongo_client`: http://api.mongodb.com/python/current/api/pymongo/mongo_client.html +.. _`PyMongo mongo_client`: https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html diff --git a/docs/tutorials/custom_idfields.rst b/docs/tutorials/custom_idfields.rst index b0f3097fb..4b3de4703 100644 --- a/docs/tutorials/custom_idfields.rst +++ b/docs/tutorials/custom_idfields.rst @@ -135,6 +135,13 @@ supposed to pass the value, like so: POST {"name":"bill", "_id":"48c00ee9-4dbe-413f-9fc3-d5f12a91de1c"} +.. note:: + By default, Eve sets PyMongo's ``UuidRepresentation`` to ``standard``. + This allows for seamlessly handling of modern Python-generated UUID values. You + can change the default by setting the ``uuidRepresentation`` value of ``MONGO_OPTIONS`` + as desired. For more informations, see `PyMongo documentation`_. + .. _`custom url converters`: http://werkzeug.pocoo.org/docs/routing/#custom-converters .. _Flask: http://flask.pocoo.org/ .. _Werkzeug: http://werkzeug.pocoo.org/ +.. _PyMongo documentation: https://pymongo.readthedocs.io/en/stable/examples/uuid.html#configuring-uuid-representation diff --git a/eve/default_settings.py b/eve/default_settings.py index 9c3b38250..c5d37081a 100644 --- a/eve/default_settings.py +++ b/eve/default_settings.py @@ -11,6 +11,9 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. + .. versionchanged:: 2.0 + 'MONGO_OPTIONS', 'uuidRepresentation' option added. + .. versionchanged:: 1.1.0 'MONGO_QUERY_WHITELIST' added and set to emtpy list. @@ -268,7 +271,7 @@ # Explicitly set default write_concern to 'safe' (do regular # aknowledged writes). This is also the current PyMongo/Mongo default setting. MONGO_WRITE_CONCERN = {"w": 1} -MONGO_OPTIONS = {"connect": True, "tz_aware": True} +MONGO_OPTIONS = {"connect": True, "tz_aware": True, "uuidRepresentation": "standard"} # if true, the document will be normalized according to the schema during patch # this means fields will be reset their the default value, if any, unless diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index 5597a389c..66812ac51 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -10,7 +10,7 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ - +from bson import UuidRepresentation from flask import current_app from pymongo import MongoClient, uri_parser @@ -73,13 +73,12 @@ def config_to_kwargs(mapping): client_kwargs["port"] = app.config[key("PORT")] client_kwargs["host"] = host + client_kwargs["authSource"] = dbname if key("DOCUMENT_CLASS") in app.config: client_kwargs["document_class"] = app.config[key("DOCUMENT_CLASS")] - cx = MongoClient(**client_kwargs) - db = cx[dbname] - + auth_kwargs = {} if key("USERNAME") in app.config: app.config.setdefault(key("PASSWORD"), None) username = app.config[key("USERNAME")] @@ -87,14 +86,18 @@ def config_to_kwargs(mapping): auth = (username, password) if any(auth) and not all(auth): raise Exception("Must set both USERNAME and PASSWORD or neither") + client_kwargs["username"] = username + client_kwargs["password"] = password if any(auth): auth_mapping = { - "AUTH_MECHANISM": "mechanism", - "AUTH_SOURCE": "source", + "AUTH_MECHANISM": "authMechanism", + "AUTH_SOURCE": "authSource", "AUTH_MECHANISM_PROPERTIES": "authMechanismProperties", } auth_kwargs = config_to_kwargs(auth_mapping) - db.authenticate(username, password, **auth_kwargs) + + cx = MongoClient(**{**client_kwargs, **auth_kwargs}) + db = cx[dbname] app.extensions["pymongo"][config_prefix] = (cx, db) diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 62b78ebf1..07883a0a0 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -59,7 +59,7 @@ def fs(self, resource=None): px = driver.current_mongo_prefix(resource) if px not in self._fs: - self._fs[px] = GridFS(driver.pymongo(prefix=px).db, disable_md5=True) + self._fs[px] = GridFS(driver.pymongo(prefix=px).db) return self._fs[px] def get(self, _id, resource=None): diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 3d5a024cb..49c6d72dd 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -7,8 +7,8 @@ import os import simplejson as json from datetime import datetime, timedelta -from pymongo import MongoClient from bson import ObjectId +from pymongo import MongoClient from eve.tests.test_settings import ( MONGO_PASSWORD, MONGO_USERNAME, @@ -360,7 +360,9 @@ def assert500(self, status): self.assertEqual(status, 500) def setupDB(self): - self.connection = MongoClient(MONGO_HOST, MONGO_PORT) + self.connection = MongoClient( + MONGO_HOST, MONGO_PORT, uuidRepresentation="standard" + ) self.connection.drop_database(MONGO_DBNAME) if MONGO_USERNAME: db = self.connection[MONGO_DBNAME] @@ -623,4 +625,3 @@ def bulk_insert(self): _db.internal_transactions.insert_many(self.random_internal_transactions(4)) products = self.generate_products() _db.products.insert_many(products) - self.connection.close() diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 59516f4b1..4ed26628b 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import pytest import simplejson as json from werkzeug.routing import BaseConverter from eve.tests import TestBase, TestMinimal diff --git a/eve/tests/io/__init__.py b/eve/tests/io/__init__.py deleted file mode 100644 index c516042f2..000000000 --- a/eve/tests/io/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# import hack so modules importing this package can still import BytesIO from -# standard library's io module -from io import BytesIO # noqa diff --git a/eve/tests/test_io/__init__.py b/eve/tests/test_io/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/eve/tests/test_io/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/eve/tests/io/flask_pymongo.py b/eve/tests/test_io/flask_pymongo.py similarity index 92% rename from eve/tests/io/flask_pymongo.py rename to eve/tests/test_io/flask_pymongo.py index 1b3787fbe..d120dffda 100644 --- a/eve/tests/io/flask_pymongo.py +++ b/eve/tests/test_io/flask_pymongo.py @@ -1,3 +1,5 @@ +import pytest + from eve.tests import TestBase from pymongo import MongoClient from pymongo.errors import OperationFailure @@ -41,9 +43,14 @@ def test_auth_params_provided_in_config(self): def test_invalid_auth_params_provided(self): # if bad username and/or password is provided in MONGO_URL and mongo # run w\o --auth pymongo won't raise exception + def func(): + with self.app.app_context(): + db = PyMongo(self.app, "MONGO1").db + db.works.find_one() + self.app.config["MONGO1_USERNAME"] = "bad_username" self.app.config["MONGO1_PASSWORD"] = "bad_password" - self.assertRaises(OperationFailure, self._pymongo_instance) + self.assertRaises(OperationFailure, func) def test_invalid_port(self): self.app.config["MONGO1_PORT"] = "bad_value" diff --git a/eve/tests/io/media.py b/eve/tests/test_io/media.py similarity index 100% rename from eve/tests/io/media.py rename to eve/tests/test_io/media.py diff --git a/eve/tests/io/mongo.py b/eve/tests/test_io/mongo.py similarity index 100% rename from eve/tests/io/mongo.py rename to eve/tests/test_io/mongo.py diff --git a/eve/tests/io/multi_mongo.py b/eve/tests/test_io/multi_mongo.py similarity index 97% rename from eve/tests/io/multi_mongo.py rename to eve/tests/test_io/multi_mongo.py index 49217eb1e..2d500a010 100644 --- a/eve/tests/io/multi_mongo.py +++ b/eve/tests/test_io/multi_mongo.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from datetime import datetime +import pytest import simplejson as json from bson import ObjectId from pymongo import MongoClient @@ -57,7 +58,6 @@ def bulk_insert2(self): works = self.random_works(self.known_resource_count) _db.works.insert_many(works) self.work = _db.works.find_one() - self.connection.close() def random_works(self, num): works = [] @@ -94,7 +94,6 @@ def test_post_multidb(self): id_field = self.domain["works"]["id_field"] new = db.works.find_one({id_field: ObjectId(work[id_field])}) self.assertTrue(new is not None) - self.connection.close() # while 'contacts' endpoint stores data to MONGO contact = {"ref": "1234567890123456789054321"} @@ -104,7 +103,6 @@ def test_post_multidb(self): id_field = self.domain["contacts"]["id_field"] new = db.contacts.find_one({id_field: ObjectId(r[id_field])}) self.assertTrue(new is not None) - self.connection.close() def test_patch_multidb(self): # test that a PATCH on 'works' udpates data on MONGO1 @@ -122,7 +120,6 @@ def test_patch_multidb(self): db = self.connection[MONGO1_DBNAME] updated = db.works.find_one({id_field: ObjectId(id)}) self.assertEqual(updated["author"], "mike") - self.connection.close() # while 'contacts' endpoint updates data on MONGO field, value = "ref", "1234567890123456789012345" @@ -137,7 +134,6 @@ def test_patch_multidb(self): db = self.connection[MONGO_DBNAME] updated = db.contacts.find_one({id_field: ObjectId(self.item_id)}) self.assertEqual(updated[field], value) - self.connection.close() def test_put_multidb(self): # test that a PUT on 'works' udpates data on MONGO1 @@ -155,7 +151,6 @@ def test_put_multidb(self): db = self.connection[MONGO1_DBNAME] updated = db.works.find_one({id_field: ObjectId(id)}) self.assertEqual(updated["author"], "mike") - self.connection.close() # while 'contacts' endpoint updates data on MONGO field, value = "ref", "1234567890123456789012345" @@ -170,7 +165,6 @@ def test_put_multidb(self): db = self.connection[MONGO_DBNAME] updated = db.contacts.find_one({id_field: ObjectId(self.item_id)}) self.assertEqual(updated[field], value) - self.connection.close() def test_delete_multidb(self): # test that DELETE on 'works' deletes data on MONGO1 @@ -182,7 +176,6 @@ def test_delete_multidb(self): db = self.connection[MONGO1_DBNAME] lost = db.works.find_one({id_field: ObjectId(id)}) self.assertEqual(lost, None) - self.connection.close() # while 'contacts' still deletes on MONGO r = self.test_client.delete( @@ -193,7 +186,6 @@ def test_delete_multidb(self): id_field = self.domain["contacts"]["id_field"] lost = db.contacts.find_one({id_field: ObjectId(self.item_id)}) self.assertEqual(lost, None) - self.connection.close() def test_create_index_with_mongo_uri_and_prefix(self): self.app.config["MONGO_URI"] = "mongodb://%s:%s/%s" % ( diff --git a/eve/tests/logging.py b/eve/tests/test_logging.py similarity index 100% rename from eve/tests/logging.py rename to eve/tests/test_logging.py diff --git a/eve/utils.py b/eve/utils.py index d1cc8505a..9c028b8d0 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -13,6 +13,8 @@ import sys from importlib import import_module +from bson import UuidRepresentation + import eve import hashlib import werkzeug.exceptions @@ -336,6 +338,19 @@ def document_etag(value, ignore_fields=None): Using bson.json_util.dumps over str(value) to make etag computation consistent between different runs and/or server instances (#16). """ + + def uuid_representation_as_string(): + uuid_map = { + "standard": UuidRepresentation.STANDARD, + "unspecified": UuidRepresentation.UNSPECIFIED, + "pythonLegacy": UuidRepresentation.PYTHON_LEGACY, + "csharpLegacy": UuidRepresentation.CSHARP_LEGACY, + "javaLegacy": UuidRepresentation.JAVA_LEGACY, + } + return uuid_map[ + config.MONGO_OPTIONS.get("uuidRepresentation", UuidRepresentation.STANDARD) + ] + if ignore_fields: def filter_ignore_fields(d, fields): @@ -359,8 +374,17 @@ def filter_ignore_fields(d, fields): h = hashlib.sha1() json_encoder = app.data.json_encoder_class() + from bson.json_util import DEFAULT_JSON_OPTIONS + h.update( - dumps(value_, sort_keys=True, default=json_encoder.default).encode("utf-8") + dumps( + value_, + sort_keys=True, + default=json_encoder.default, + json_options=DEFAULT_JSON_OPTIONS.with_options( + uuid_representation=uuid_representation_as_string() + ), + ).encode("utf-8") ) return h.hexdigest() diff --git a/setup.py b/setup.py index 0b80d549a..b777dc7d2 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", "flask", - "pymongo>=3.7,<4.0", + "pymongo", "simplejson>=3.3.0,<4.0", ] From 4548c5c18cd8c161db9800aba9a88a94ccf57428 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 13 Feb 2022 10:20:46 +0100 Subject: [PATCH 715/821] fix: tutorial mistake on custom IDs with UUIDs. Closes #1451 --- CHANGES.rst | 2 ++ docs/tutorials/custom_idfields.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9e6c084b1..bd58cb653 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -39,9 +39,11 @@ Fixed - Prepare for Python 3 switch (`#1445`_) - Update docs and tests regarding pagination of empty resources (`#1463`_) - Fix fork link in contributing info (`#1447`_) +- Tutorial mistake on custom IDs values with UUIDs (`#1451`_) - Documentation typos (`#1462`_) - Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) +.. _`#1451`: https://github.com/pyeve/eve/issues/1451 .. _`#1464`: https://github.com/pyeve/eve/issues/1464 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 .. _`#1439`: https://github.com/pyeve/eve/pull/1439 diff --git a/docs/tutorials/custom_idfields.rst b/docs/tutorials/custom_idfields.rst index 4b3de4703..980210e8f 100644 --- a/docs/tutorials/custom_idfields.rst +++ b/docs/tutorials/custom_idfields.rst @@ -85,6 +85,7 @@ details on custom validation): def _validate_type_uuid(self, value): try: UUID(value) + return True except ValueError: pass From e2b62367b2ee955d385fa6523dcc590485285de7 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 8 Mar 2022 14:28:32 +0100 Subject: [PATCH 716/821] Fix small typo on `quickstart.rst` --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 265a6e448..49c8afe9c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -102,7 +102,7 @@ Try requesting ``people`` now: This time we also got an ``_items`` list. The ``_links`` are relative to the resource being accessed, so you get a link to the parent resource (the home page) and to the resource itself. If you got a timeout error from pymongo, make -sure the prerequistes are met. Chances are that the ``mongod`` server process +sure the prerequisites are met. Chances are that the ``mongod`` server process is not running. By default Eve APIs are read-only: From 8a27bfecd77b263fcf22cafaa4fc2fcd1291a017 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 8 Mar 2022 17:23:33 +0100 Subject: [PATCH 717/821] changelog for #1469 --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bd58cb653..4a651fad3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -40,9 +40,10 @@ Fixed - Update docs and tests regarding pagination of empty resources (`#1463`_) - Fix fork link in contributing info (`#1447`_) - Tutorial mistake on custom IDs values with UUIDs (`#1451`_) -- Documentation typos (`#1462`_) +- Documentation typos (`#1462`_, `#1469`_) - Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) +.. _`#1469`: https://github.com/pyeve/eve/pull/1469 .. _`#1451`: https://github.com/pyeve/eve/issues/1451 .. _`#1464`: https://github.com/pyeve/eve/issues/1464 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 From 3c1bb64b3a9d4efcf915d19a9e4f5e7cd49b6191 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 8 Mar 2022 17:24:28 +0100 Subject: [PATCH 718/821] Marcelo Trylesinski --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 29c6e0abf..adea51cef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -116,6 +116,7 @@ Patches and Contributions - Mandar Vaze - Manquer - Marc Abramowitz +- Marcelo Trylesinski - Marcin Puhacz - Marcus Cobden - Marica Odagaki From c20aa0ce8f7223f1500b19e1281907f46afed0bf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Apr 2022 14:52:56 +0200 Subject: [PATCH 719/821] fix: starting with Werkzeug 2.1, HATEOAS links are relative Closes #1475 --- CHANGES.rst | 2 ++ eve/render.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4a651fad3..29776d104 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -42,8 +42,10 @@ Fixed - Tutorial mistake on custom IDs values with UUIDs (`#1451`_) - Documentation typos (`#1462`_, `#1469`_) - Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) +- Starting with Werkzeug 2.1, HATEOAS links are relative instead of absolute (`#1475`_) .. _`#1469`: https://github.com/pyeve/eve/pull/1469 +.. _`#1475`: https://github.com/pyeve/eve/issues/1475 .. _`#1451`: https://github.com/pyeve/eve/issues/1451 .. _`#1464`: https://github.com/pyeve/eve/issues/1464 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 diff --git a/eve/render.py b/eve/render.py index f53839283..e9a4acae5 100644 --- a/eve/render.py +++ b/eve/render.py @@ -158,6 +158,7 @@ def _prepare_response( # build the main wsgi response object resp = make_response(rendered, status) resp.mimetype = mime + resp.autocorrect_location_header = True # extra headers if headers: From 9ae14e5c6081fc8202180ad28776ebb69c70b80e Mon Sep 17 00:00:00 2001 From: David Booss Date: Fri, 8 Apr 2022 21:00:10 -0700 Subject: [PATCH 720/821] render.py: use markupsafe since werkzeug has removed escape from its utils: https://github.com/pallets/werkzeug/commit/22d1e9ac13829b83347107a9b4d77072a8e1af6a --- eve/render.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/eve/render.py b/eve/render.py index e9a4acae5..b542c2592 100644 --- a/eve/render.py +++ b/eve/render.py @@ -15,6 +15,7 @@ import datetime import simplejson as json from werkzeug import utils +from markupsafe import escape from functools import wraps from eve.methods.common import get_rate_limit from eve.utils import ( @@ -386,7 +387,7 @@ def xml_root_open(cls, data): href = title = "" if links and "self" in links: self_ = links.pop("self") - href = ' href="%s" ' % utils.escape(self_["href"]) + href = ' href="%s" ' % escape(self_["href"]) if "title" in self_: title = ' title="%s" ' % self_["title"] return "" % (href, title) @@ -444,11 +445,11 @@ def xml_add_links(cls, data): elif isinstance(link, list): xml += "".join( - chunk % (rel, utils.escape(d["href"]), utils.escape(d["title"])) + chunk % (rel, escape(d["href"]), escape(d["title"])) for d in link ) else: - xml += "".join(chunk % (rel, utils.escape(link["href"]), link["title"])) + xml += "".join(chunk % (rel, escape(link["href"]), link["title"])) return xml @classmethod @@ -525,7 +526,7 @@ def xml_dict(cls, data): xml += cls.xml_field_close(k) else: xml += cls.xml_field_open(k, idx, related_links) - xml += "%s" % utils.escape(value) + xml += "%s" % escape(value) xml += cls.xml_field_close(k) return xml @@ -543,13 +544,13 @@ def xml_field_open(cls, field, idx, related_links): if isinstance(related_links[field], list): return '<%s href="%s" title="%s">' % ( field, - utils.escape(related_links[field][idx]["href"]), + escape(related_links[field][idx]["href"]), related_links[field][idx]["title"], ) else: return '<%s href="%s" title="%s">' % ( field, - utils.escape(related_links[field]["href"]), + escape(related_links[field]["href"]), related_links[field]["title"], ) else: From 6ae332da98fa2937635681e28119287fe063e81a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Apr 2022 15:03:11 +0200 Subject: [PATCH 721/821] changelog update for #1473 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 29776d104..4ed153874 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,8 @@ New Fixed ~~~~~ +- AttributeError: module 'werkzeug.utils' has no attribute 'escape' (`#1474`_) +- Starting with Werkzeug 2.1, HATEOAS links are relative instead of absolute (`#1475`_) - Eve doesn't work with latest PyMongo (v4) (`#1461`_, `#1464`_) - Fix 500 error with empty token/bearer (`#1456`_) - Do not return related fields if field is a empty list (`#1441`_) @@ -42,10 +44,10 @@ Fixed - Tutorial mistake on custom IDs values with UUIDs (`#1451`_) - Documentation typos (`#1462`_, `#1469`_) - Switch to GitHub Actions from Travis CI (`#1439`_, `#1444`_) -- Starting with Werkzeug 2.1, HATEOAS links are relative instead of absolute (`#1475`_) .. _`#1469`: https://github.com/pyeve/eve/pull/1469 .. _`#1475`: https://github.com/pyeve/eve/issues/1475 +.. _`#1474`: https://github.com/pyeve/eve/issues/1474 .. _`#1451`: https://github.com/pyeve/eve/issues/1451 .. _`#1464`: https://github.com/pyeve/eve/issues/1464 .. _`#1461`: https://github.com/pyeve/eve/issues/1461 From 2f49a065deed83be01bf24950769da2afdc74fed Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Apr 2022 15:03:21 +0200 Subject: [PATCH 722/821] David Booss --- AUTHORS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index adea51cef..5d47ce78c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,7 @@ Patches and Contributions - Daniele Pizzolli - Danse - David Arnold +- David Booss - David Buchmann - David Murphy - David Wood @@ -116,7 +117,7 @@ Patches and Contributions - Mandar Vaze - Manquer - Marc Abramowitz -- Marcelo Trylesinski +- Marcelo Trylesinski - Marcin Puhacz - Marcus Cobden - Marica Odagaki @@ -210,4 +211,4 @@ Patches and Contributions - mmizotin - quentinpraz - smeng9 -- xgdgsc +- xgdgsc \ No newline at end of file From 2240af53dfc20cbef9c5d2e7713a9481ec4817a7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 13 Apr 2022 15:17:07 +0200 Subject: [PATCH 723/821] bump version to 2.0.dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 1e4b4f315..991dae280 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "1.1.6.dev0" +__version__ = "2.0.dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 6f6aacb848d2a33898fb6f460647de3276bd1558 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 8 Jun 2022 15:19:08 +0200 Subject: [PATCH 724/821] bump version to 2.0 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4ed153874..b09128033 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- his sunt leones. + +Version v2.0 +------------ + +Released on Jun 8, 2022. + Breaking ~~~~~~~~ Starting from this release, Eve supports Python 3.7 and above. diff --git a/eve/__init__.py b/eve/__init__.py index 991dae280..790451e72 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.dev0" +__version__ = "2.0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 244bed2058a9f63c0a26792b14d93196b850ff20 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 17 Jul 2022 23:24:25 +1000 Subject: [PATCH 725/821] docs: Fix a few typos There are small typos in: - docs/tutorials/account_management.rst - eve/methods/delete.py - eve/tests/config.py - eve/tests/methods/patch.py - eve/tests/methods/post.py Fixes: - Should read `occurrence` rather than `occurence`. - Should read `existence` rather than `existance`. - Should read `enabled` rather than `eanbled`. - Should read `default` rather than `defult`. - Should read `devolution` rather than `develtion`. Signed-off-by: Tim Gates --- docs/tutorials/account_management.rst | 2 +- eve/methods/delete.py | 2 +- eve/tests/config.py | 2 +- eve/tests/methods/patch.py | 2 +- eve/tests/methods/post.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/account_management.rst b/docs/tutorials/account_management.rst index 3ebf7dc38..fba0e6bea 100644 --- a/docs/tutorials/account_management.rst +++ b/docs/tutorials/account_management.rst @@ -5,7 +5,7 @@ RESTful Account Management This tutorial assumes that you've read the :ref:`quickstart` and the :ref:`auth` guides. -Except for the relatively rare occurence of open (and generally read-only) public +Except for the relatively rare occurrence of open (and generally read-only) public APIs, most services are only accessible to authenticated users. A common pattern is that users create their account on a website or with a mobile application. Once they have an account, they are allowed to consume one or more diff --git a/eve/methods/delete.py b/eve/methods/delete.py index c740adc37..7482297b2 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -204,7 +204,7 @@ def delete(resource, **lookup): 'on_deleted_resource' raised after performing the delete .. versionchanged:: 0.3 - Support for the lookup filter, which allows for develtion of + Support for the lookup filter, which allows for devolution of sub-resources (only delete documents that match a given condition). .. versionchanged:: 0.0.4 diff --git a/eve/tests/config.py b/eve/tests/config.py index 9d5cdd8b9..9d4d08aea 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -424,7 +424,7 @@ def test_auth_field_as_custom_idfield(self): def test_oplog_config(self): - # if OPLOG_ENDPOINT is eanbled the endoint is included with the domain + # if OPLOG_ENDPOINT is enabled the endoint is included with the domain self.app.config["OPLOG_ENDPOINT"] = "oplog" self.app._init_oplog() self.assertOplog("oplog", "oplog") diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 6a4d2b690..7f27e971d 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -739,7 +739,7 @@ def test_patch_nested_document_nullable_missing(self): def test_patch_dependent_field_on_origin_document(self): """Test that when patching a field which is dependent on another field's - existance, and this other field is not provided in the patch, but does + existence, and this other field is not provided in the patch, but does exist on the persisted document, the patch will be accepted. The value on the document can be there either because is was set diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 9f76b695c..fcc0411d3 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -828,7 +828,7 @@ def test_post_with_nested_default(self): def test_post_readonly_in_dict(self): # Test that a post with a readonly field inside a dict is properly - # validated (even if it has a defult value) + # validated (even if it has a default value) del self.domain["contacts"]["schema"]["ref"]["required"] test_field = "dict_with_read_only" test_value = {"read_only_in_dict": "default"} From 085fe9deea3b03f925457f062193cc1c6385f00d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 18 Jul 2022 08:59:53 +0200 Subject: [PATCH 726/821] changelog for #1481 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b09128033..d6ee9f2dd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- his sunt leones. +- Fix documentation typos (`#1481`_) + +.. _`#1781`: https://github.com/pyeve/eve/pull/1481 Version v2.0 ------------ From 87231307688af84b9c166aad41ddb0aa33a83e07 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 18 Jul 2022 09:04:32 +0200 Subject: [PATCH 727/821] fix broken link --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d6ee9f2dd..be8d7b3ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,7 @@ In Development - Fix documentation typos (`#1481`_) -.. _`#1781`: https://github.com/pyeve/eve/pull/1481 +.. _`#1481`: https://github.com/pyeve/eve/pull/1481 Version v2.0 ------------ From c8af0981f3427c804f5a379252515d9c66ffd2f5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 1 Aug 2022 10:53:20 +0200 Subject: [PATCH 728/821] bump version to 2.0.1-dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 790451e72..faadcb76f 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0" +__version__ = "2.0.1-dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 879a666d21776c52b037bf746b8a58f21515b118 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 3 Sep 2022 08:08:34 +0200 Subject: [PATCH 729/821] lock Flask dependency to <2.2 --- CHANGES.rst | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index be8d7b3ff..be5b8c566 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Lock Flask dependency to <2.2. - Fix documentation typos (`#1481`_) .. _`#1481`: https://github.com/pyeve/eve/pull/1481 diff --git a/setup.py b/setup.py index b777dc7d2..b0389cf5c 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ INSTALL_REQUIRES = [ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", - "flask", + "flask<2.2", "pymongo", "simplejson>=3.3.0,<4.0", ] From bd522761dd5b3a907a249238a598324896805794 Mon Sep 17 00:00:00 2001 From: shaoyu Date: Thu, 21 Jul 2022 14:11:39 -0500 Subject: [PATCH 730/821] fix auth using embedded username/password --- eve/io/mongo/flask_pymongo.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index 66812ac51..f0ad8a49f 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -64,16 +64,29 @@ def config_to_kwargs(mapping): host = app.config[key("URI")] # raises an exception if uri is invalid mongo_settings = uri_parser.parse_uri(host) + + # extract username and password from uri + if mongo_settings.get("username"): + client_kwargs["username"] = mongo_settings["username"] + client_kwargs["password"] = mongo_settings["password"] + + # extract default database from uri dbname = mongo_settings.get("database") if not dbname: dbname = app.config[key("DBNAME")] + + # extract auth source from uri + auth_source = mongo_settings["options"].get("authSource") + if not auth_source: + auth_source = dbname else: dbname = app.config[key("DBNAME")] + auth_source = dbname host = app.config[key("HOST")] client_kwargs["port"] = app.config[key("PORT")] client_kwargs["host"] = host - client_kwargs["authSource"] = dbname + client_kwargs["authSource"] = auth_source if key("DOCUMENT_CLASS") in app.config: client_kwargs["document_class"] = app.config[key("DOCUMENT_CLASS")] From 18770a9eb74d128ae9b37555ab7e655bd3539ea1 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 3 Sep 2022 08:08:34 +0200 Subject: [PATCH 731/821] lock Flask dependency to <2.2 --- CHANGES.rst | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index be8d7b3ff..525e4caba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Lock Flask dependency to version 2.1. - Fix documentation typos (`#1481`_) .. _`#1481`: https://github.com/pyeve/eve/pull/1481 diff --git a/setup.py b/setup.py index b777dc7d2..b0389cf5c 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ INSTALL_REQUIRES = [ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", - "flask", + "flask<2.2", "pymongo", "simplejson>=3.3.0,<4.0", ] From becbeb49580f64f25bd4282762fbeae94181aaf8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 3 Sep 2022 08:25:50 +0200 Subject: [PATCH 732/821] changelog for #1482 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 525e4caba..d925d0336 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Fix: MONGO_URI's username, password, and authSource in uri are not parsed correctly (`#1478`_) - Lock Flask dependency to version 2.1. - Fix documentation typos (`#1481`_) +.. _`#1478`: https://github.com/pyeve/eve/issues/1478 .. _`#1481`: https://github.com/pyeve/eve/pull/1481 Version v2.0 From 4f10d3d2f88f3f5e82f5acaa79eb5175996cb895 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sun, 4 Sep 2022 08:15:24 +0200 Subject: [PATCH 733/821] changelog cleanup --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d925d0336..c8aefd3f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Fix: MONGO_URI's username, password, and authSource in uri are not parsed correctly (`#1478`_) +- MONGO_URI's username, password, and authSource are not parsed correctly (`#1478`_) - Lock Flask dependency to version 2.1. - Fix documentation typos (`#1481`_) From 15c7d94407e29de2468dec981cd252ad37398305 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Sep 2022 16:48:34 +0200 Subject: [PATCH 734/821] only build py3 wheels --- CHANGES.rst | 1 + setup.cfg | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 setup.cfg diff --git a/CHANGES.rst b/CHANGES.rst index c8aefd3f3..bfb416b96 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ In Development - MONGO_URI's username, password, and authSource are not parsed correctly (`#1478`_) - Lock Flask dependency to version 2.1. - Fix documentation typos (`#1481`_) +- Only build Python 3 wheels. .. _`#1478`: https://github.com/pyeve/eve/issues/1478 .. _`#1481`: https://github.com/pyeve/eve/pull/1481 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf13d..000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 From 27bb1f59ababa4cec611b4860f08c9dcf3a82aa6 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Sep 2022 15:43:48 +0200 Subject: [PATCH 735/821] bump version to 2.0.1 --- CHANGES.rst | 15 +++++++++++++-- eve/__init__.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bfb416b96..699cf6452 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,11 +6,22 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- MONGO_URI's username, password, and authSource are not parsed correctly (`#1478`_) -- Lock Flask dependency to version 2.1. +- *hic sunt dracones* + +Version v2.0.1 +-------------- + +Released on Sep 7, 2022. + +Fixed +~~~~~ + +- ``MONGO_URI`` username, password, and authSource are not parsed correctly (`#1478`_) +- Lock Flask dependency to version 2.1 (`#1485`_) - Fix documentation typos (`#1481`_) - Only build Python 3 wheels. +.. _`#1485`: https://github.com/pyeve/eve/issues/1485 .. _`#1478`: https://github.com/pyeve/eve/issues/1478 .. _`#1481`: https://github.com/pyeve/eve/pull/1481 diff --git a/eve/__init__.py b/eve/__init__.py index faadcb76f..cf38e5683 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.1-dev0" +__version__ = "2.0.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 3dbb53d24dad522f52e7de574c45e07e4eeb613c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 7 Sep 2022 16:59:05 +0200 Subject: [PATCH 736/821] bump version to 2.0.2-dev0 --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index cf38e5683..446b80942 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.1" +__version__ = "2.0.2-dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From dd97531e80e0a651b199f27a0da7dfd8f66d3069 Mon Sep 17 00:00:00 2001 From: tgm Date: Wed, 21 Sep 2022 16:51:53 +0200 Subject: [PATCH 737/821] Fix issue #1486 This solution seems to fit the current code best, but alternatively one could get the value earlier and return `UuidRepresentation.STANDARD` early if the value is of a non-string type. --- eve/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/utils.py b/eve/utils.py index 9c028b8d0..d5fccdad2 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -348,7 +348,7 @@ def uuid_representation_as_string(): "javaLegacy": UuidRepresentation.JAVA_LEGACY, } return uuid_map[ - config.MONGO_OPTIONS.get("uuidRepresentation", UuidRepresentation.STANDARD) + config.MONGO_OPTIONS.get("uuidRepresentation", "standard") ] if ignore_fields: From ee88650d9fb3ec895e6ef56a0a5fe7f3d268f7a5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 23 Sep 2022 09:25:23 +0200 Subject: [PATCH 738/821] tgm --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 5d47ce78c..22af1eed6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -211,4 +211,5 @@ Patches and Contributions - mmizotin - quentinpraz - smeng9 +- tgm - xgdgsc \ No newline at end of file From 94329018b6d118c58dda6de1566baf5077d43d73 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 23 Sep 2022 09:27:35 +0200 Subject: [PATCH 739/821] changelog for #1487 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 699cf6452..f4ad182a6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt dracones* +- Fix: etag generation fails if ``uuidRepresentation`` is not set in MONGO_OPTIONS (`#1486`_) + +.. _`#1486`: https://github.com/pyeve/eve/issues/1486 Version v2.0.1 -------------- From 7dfd2d1e710634616e8e3a5b7f2733050c21de83 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 23 Sep 2022 09:29:58 +0200 Subject: [PATCH 740/821] bump version to 2.0.2 --- CHANGES.rst | 8 ++++++++ eve/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f4ad182a6..51867f9eb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,14 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +Version v2.0.2 +-------------- + +Released on Sep 23, 2022. + +Fixed +~~~~~ + - Fix: etag generation fails if ``uuidRepresentation`` is not set in MONGO_OPTIONS (`#1486`_) .. _`#1486`: https://github.com/pyeve/eve/issues/1486 diff --git a/eve/__init__.py b/eve/__init__.py index 446b80942..f01326e61 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.2-dev0" +__version__ = "2.0.2" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 7c4fc9e31e472db8ef8acedd5bb51b71f071d76a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 23 Sep 2022 09:43:45 +0200 Subject: [PATCH 741/821] bump version to 2.0.3-dev0 --- CHANGES.rst | 2 ++ eve/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 51867f9eb..d13ff1cfc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- *hic sunt dracones* + Version v2.0.2 -------------- diff --git a/eve/__init__.py b/eve/__init__.py index f01326e61..b8e9e3081 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.2" +__version__ = "2.0.3-dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 245bb4a17fc1936083c053040c9be55ff505213b Mon Sep 17 00:00:00 2001 From: Luis Fernando Gomes Date: Wed, 26 Oct 2022 17:22:33 -0300 Subject: [PATCH 742/821] Fix _update field missing timezone Fix #1490 --- eve/methods/common.py | 6 +++++- eve/methods/delete.py | 4 ++-- eve/methods/patch.py | 4 ++-- eve/methods/post.py | 4 ++-- eve/methods/put.py | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 9398f65bc..46a0783e8 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -13,7 +13,7 @@ import base64 import time from copy import copy -from datetime import datetime +from datetime import datetime, timezone from functools import wraps import simplejson as json @@ -1538,3 +1538,7 @@ def oplog_push(resource, document, op, id=None): getattr(app, "on_oplog_push")(resource, entries) # oplog push app.data.insert(config.OPLOG_NAME, entries) + + +def utcnow(): + return datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) diff --git a/eve/methods/delete.py b/eve/methods/delete.py index 7482297b2..b906e8838 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -19,6 +19,7 @@ pre_event, oplog_push, resolve_document_etag, + utcnow, ) from eve.versioning import ( versioned_id_field, @@ -26,7 +27,6 @@ insert_versioning_documents, late_versioning_catch, ) -from datetime import datetime import copy @@ -118,7 +118,7 @@ def deleteitem_internal( marked_document = copy.deepcopy(original) # Set DELETED flag and update metadata - last_modified = datetime.utcnow().replace(microsecond=0) + last_modified = utcnow() marked_document[config.DELETED] = True marked_document[config.LAST_UPDATED] = last_modified diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 2589b352a..61851eae4 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -13,7 +13,6 @@ from copy import deepcopy from flask import current_app as app, abort from werkzeug import exceptions -from datetime import datetime from eve.utils import config, debug_error_message, parse_request from eve.auth import requires_auth from cerberus.validator import DocumentError @@ -29,6 +28,7 @@ marshal_write_response, resolve_document_etag, oplog_push, + utcnow, ) from eve.versioning import ( resolve_document_version, @@ -198,7 +198,7 @@ def patch_internal( resolve_document_version(updates, resource, "PATCH", original) # some datetime precision magic - updates[config.LAST_UPDATED] = datetime.utcnow().replace(microsecond=0) + updates[config.LAST_UPDATED] = utcnow() if resource_def["soft_delete"] is True: # PATCH with soft delete enabled should always set the DELETED diff --git a/eve/methods/post.py b/eve/methods/post.py index 0c37c4c2c..93e3b47e2 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -11,7 +11,6 @@ :license: BSD, see LICENSE for more details. """ -from datetime import datetime from flask import current_app as app, abort from eve.utils import config, parse_request, debug_error_message from eve.auth import requires_auth @@ -30,6 +29,7 @@ resolve_document_etag, oplog_push, resource_link, + utcnow, ) from eve.versioning import resolve_document_version, insert_versioning_documents @@ -157,7 +157,7 @@ def post_internal(resource, payl=None, skip_validation=False): JSON links. Superflous ``response`` container removed. """ - date_utc = datetime.utcnow().replace(microsecond=0) + date_utc = utcnow() resource_def = app.config["DOMAIN"][resource] schema = resource_def["schema"] validator = ( diff --git a/eve/methods/put.py b/eve/methods/put.py index 1cf871a0b..1810a0197 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -9,7 +9,6 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -from datetime import datetime from flask import current_app as app, abort from werkzeug import exceptions @@ -29,6 +28,7 @@ resolve_sub_resource_path, resolve_document_etag, oplog_push, + utcnow, ) from eve.methods.post import post_internal from eve.utils import config, debug_error_message, parse_request @@ -188,7 +188,7 @@ def put_internal( late_versioning_catch(original, resource) # update meta - last_modified = datetime.utcnow().replace(microsecond=0) + last_modified = utcnow() document[config.LAST_UPDATED] = last_modified document[config.DATE_CREATED] = original[config.DATE_CREATED] if resource_def["soft_delete"] is True: From 0158c4e9c9b015a0bf371d8a100545371089aa2d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 27 Oct 2022 11:43:01 +0200 Subject: [PATCH 743/821] more fixes for _update missing timezones (see previous commit) Addresses #1490 --- eve/methods/common.py | 2 +- eve/tests/__init__.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index 46a0783e8..220fc42da 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -1511,7 +1511,7 @@ def oplog_push(resource, document, op, id=None): if config.LAST_UPDATED in update: last_update = update[config.LAST_UPDATED] else: - last_update = datetime.utcnow().replace(microsecond=0) + last_update = utcnow() entry[config.LAST_UPDATED] = entry[config.DATE_CREATED] = last_update if config.OPLOG_AUDIT: entry["ip"] = request.remote_addr diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 49c6d72dd..1d6543ff5 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -6,7 +6,7 @@ import random import os import simplejson as json -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from bson import ObjectId from pymongo import MongoClient from eve.tests.test_settings import ( @@ -495,7 +495,7 @@ def random_contacts(self, num, standard_date_fields=True): schema = DOMAIN["contacts"]["schema"] contacts = [] for i in range(num): - dt = datetime.utcnow().replace(microsecond=0) + dt = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) contact = { "ref": self.random_string(schema["ref"]["maxlength"]), "prog": i, @@ -534,7 +534,7 @@ def random_users(self, num): def random_payments(self, num): payments = [] for i in range(num): - dt = datetime.utcnow().replace(microsecond=0) + dt = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) payment = { "a_string": self.random_string(10), "a_number": i, @@ -547,7 +547,7 @@ def random_payments(self, num): def random_invoices(self, num): invoices = [] for _ in range(num): - dt = datetime.utcnow().replace(microsecond=0) + dt = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) invoice = { "inv_number": self.random_string(10), eve.LAST_UPDATED: dt, @@ -599,7 +599,7 @@ def random_rows(self, num): def random_internal_transactions(self, num): transactions = [] for i in range(num): - dt = datetime.utcnow().replace(microsecond=0) + dt = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) transaction = { "internal_string": self.random_string(10), "internal_number": i, From 7c6468691b6579978c28861cbb1c0c1da036520b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 27 Oct 2022 11:51:53 +0200 Subject: [PATCH 744/821] changelog for #1491 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d13ff1cfc..2bfae173b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt dracones* +- Fix: malformed ``LAST_UPDATED`` field (`#1490`_) + +.. _`#1490`: https://github.com/pyeve/eve/issues/1490 Version v2.0.2 -------------- From feb1df361210db9057bd45a2654f3c22bd6fc96c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 2 Nov 2022 09:45:32 +0100 Subject: [PATCH 745/821] SECURUTY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..df8383e95 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please email pyeve at nicolaiarocci dot com any vulnerability you may find about this project. From 1ca42dc1dca2c33f555400a3e42903d7247cf39a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 2 Nov 2022 10:15:56 +0100 Subject: [PATCH 746/821] bump version to 2.0.3 --- CHANGES.rst | 10 ++++++++++ eve/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2bfae173b..70a016a68 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,16 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- hic sunt leones. + +Version v2.0.3 +-------------- + +Released on Nov 2, 2022. + +Fixed +~~~~~ + - Fix: malformed ``LAST_UPDATED`` field (`#1490`_) .. _`#1490`: https://github.com/pyeve/eve/issues/1490 diff --git a/eve/__init__.py b/eve/__init__.py index b8e9e3081..28bd6852c 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.3-dev0" +__version__ = "2.0.3" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From db0f4718b22cae9f1aedecafacf587c911924ac9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 2 Nov 2022 10:28:38 +0100 Subject: [PATCH 747/821] bump version to 2.0.4-dev0 --- CHANGES.rst | 2 +- eve/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 70a016a68..869b49af6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,7 @@ Released on Nov 2, 2022. Fixed ~~~~~ -- Fix: malformed ``LAST_UPDATED`` field (`#1490`_) +- Malformed ``LAST_UPDATED`` field (`#1490`_) .. _`#1490`: https://github.com/pyeve/eve/issues/1490 diff --git a/eve/__init__.py b/eve/__init__.py index 28bd6852c..b1e17324b 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.3" +__version__ = "2.0.4-dev0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 7ae5bddd3ebcf31e99f36df6dc2a975e08c38a1b Mon Sep 17 00:00:00 2001 From: Mark Mayo Date: Thu, 3 Nov 2022 20:04:30 +1300 Subject: [PATCH 748/821] python 3 updates and some simplifications of conditional statements. --- docs/_themes/flask_theme_support.py | 17 +---- docs/conf.py | 5 +- eve/auth.py | 9 ++- eve/endpoints.py | 15 ++-- eve/flaskapp.py | 46 ++++++------ eve/io/__init__.py | 2 +- eve/io/base.py | 17 ++--- eve/io/media.py | 2 +- eve/io/mongo/__init__.py | 2 +- eve/io/mongo/flask_pymongo.py | 2 +- eve/io/mongo/geo.py | 20 +++--- eve/io/mongo/media.py | 4 +- eve/io/mongo/mongo.py | 70 ++++++++---------- eve/io/mongo/parser.py | 7 +- eve/io/mongo/validation.py | 14 +--- eve/logging.py | 2 +- eve/methods/__init__.py | 4 +- eve/methods/common.py | 72 ++++++++----------- eve/methods/delete.py | 27 +++---- eve/methods/get.py | 33 ++++----- eve/methods/patch.py | 36 ++++------ eve/methods/post.py | 34 ++++----- eve/methods/put.py | 39 ++++------ eve/render.py | 44 ++++++------ eve/tests/__init__.py | 32 ++++----- eve/tests/auth.py | 20 +++--- eve/tests/config.py | 12 ++-- eve/tests/endpoints.py | 26 +++---- eve/tests/methods/common.py | 20 +++--- eve/tests/methods/delete.py | 11 +-- eve/tests/methods/get.py | 12 ++-- eve/tests/methods/patch.py | 10 +-- eve/tests/methods/patch_atomic_concurrency.py | 8 ++- eve/tests/methods/post.py | 19 ++--- eve/tests/methods/put.py | 11 +-- eve/tests/methods/ratelimit.py | 7 +- eve/tests/renders.py | 5 +- eve/tests/response.py | 12 ++-- eve/tests/test_io/flask_pymongo.py | 15 ++-- eve/tests/test_io/media.py | 12 ++-- eve/tests/test_io/mongo.py | 6 +- eve/tests/test_io/multi_mongo.py | 15 ++-- eve/tests/test_logging.py | 3 +- eve/tests/test_settings.py | 1 - eve/tests/utils.py | 24 +++---- eve/tests/versioning.py | 28 ++++---- eve/utils.py | 34 +++++---- eve/validation.py | 17 ++--- eve/versioning.py | 6 +- examples/notifications.py | 3 +- examples/security/bcrypt.py | 3 +- examples/security/hmac.py | 6 +- examples/security/roles.py | 6 +- examples/security/sha1-hmac.py | 6 +- examples/security/token.py | 4 +- setup.py | 4 +- 56 files changed, 412 insertions(+), 509 deletions(-) diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 0dcf53b75..9e598ba6f 100644 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,19 +1,8 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style -from pygments.token import ( - Keyword, - Name, - Comment, - String, - Error, - Number, - Operator, - Generic, - Whitespace, - Punctuation, - Other, - Literal, -) +from pygments.token import (Comment, Error, Generic, Keyword, Literal, Name, + Number, Operator, Other, Punctuation, String, + Whitespace) class FlaskyStyle(Style): diff --git a/docs/conf.py b/docs/conf.py index b8a7368b4..f71553269 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os, datetime +import datetime +import os +import sys + import alabaster # If extensions (or modules to document with autodoc) are in another directory, diff --git a/eve/auth.py b/eve/auth.py index bb63dca0f..c431f6e91 100644 --- a/eve/auth.py +++ b/eve/auth.py @@ -9,9 +9,12 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -from flask import request, current_app as app, g, abort from functools import wraps +from flask import abort +from flask import current_app as app +from flask import g, request + def requires_auth(endpoint_class): """Enables Authorization logic for decorated functions. @@ -84,7 +87,7 @@ def decorated(*args, **kwargs): return fdec -class BasicAuth(object): +class BasicAuth(): """Implements Basic AUTH logic. Should be subclassed to implement custom authentication checking. @@ -214,7 +217,7 @@ def authorized(self, allowed_roles, resource, method): try: userid, hmac_hash = auth.split(":") self.set_user_or_token(userid) - except: + except Exception: auth = None return auth and self.check_auth( userid, diff --git a/eve/endpoints.py b/eve/endpoints.py index d73ca1fab..b7c511d44 100644 --- a/eve/endpoints.py +++ b/eve/endpoints.py @@ -14,14 +14,16 @@ import re from bson import tz_util -from flask import abort, request, current_app as app, Response +from flask import Response, abort +from flask import current_app as app +from flask import request +import eve from eve.auth import requires_auth, resource_auth -from eve.methods import get, getitem, post, patch, delete, deleteitem, put +from eve.methods import delete, deleteitem, get, getitem, patch, post, put from eve.methods.common import ratelimit from eve.render import send_response -from eve.utils import config, weak_date, date_to_rfc1123 -import eve +from eve.utils import config, date_to_rfc1123, weak_date def collections_endpoint(**lookup): @@ -154,8 +156,7 @@ def home_endpoint(): response[config.LINKS] = {"child": links} return send_response(None, (response,)) - else: - return send_response(None, (response,)) + return send_response(None, (response,)) def error_endpoint(error): @@ -216,7 +217,7 @@ def media_endpoint(_id): begin, end = m.groups() begin = int(begin) end = int(end) - except: + except Exception: begin, end = 0, None length = size - begin diff --git a/eve/flaskapp.py b/eve/flaskapp.py index a372fbaed..5cb8791d5 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -9,12 +9,12 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import copy import fnmatch import os import sys import warnings -import copy from events import Events from flask import Flask from werkzeug.routing import BaseConverter @@ -22,17 +22,12 @@ import eve from eve import default_settings -from eve.endpoints import ( - collections_endpoint, - item_endpoint, - home_endpoint, - error_endpoint, - media_endpoint, - schema_collection_endpoint, - schema_item_endpoint, -) +from eve.endpoints import (collections_endpoint, error_endpoint, home_endpoint, + item_endpoint, media_endpoint, + schema_collection_endpoint, schema_item_endpoint) from eve.exceptions import ConfigException, SchemaException -from eve.io.mongo import Mongo, Validator, GridFSMediaStorage, ensure_mongo_indexes +from eve.io.mongo import (GridFSMediaStorage, Mongo, Validator, + ensure_mongo_indexes) from eve.logging import RequestFilter from eve.utils import api_prefix, extract_key_values @@ -46,7 +41,7 @@ class EveWSGIRequestHandler(WSGIRequestHandler): def server_version(self): return ( "Eve/%s " % eve.__version__ - + super(EveWSGIRequestHandler, self).server_version + + super().server_version ) @@ -54,7 +49,7 @@ class RegexConverter(BaseConverter): """Extend werkzeug routing by supporting regex for urls/API endpoints""" def __init__(self, url_map, *items): - super(RegexConverter, self).__init__(url_map) + super().__init__(url_map) self.regex = items[0] @@ -149,7 +144,7 @@ def __init__( we need to enhance our super-class a little bit. """ - super(Eve, self).__init__(import_name, **kwargs) + super().__init__(import_name, **kwargs) # add support for request metadata to the log record self.logger.addFilter(RequestFilter()) @@ -222,7 +217,7 @@ def run(self, host=None, port=None, debug=None, **options): information.""" options.setdefault("request_handler", EveWSGIRequestHandler) - super(Eve, self).run(host, port, debug, **options) + super().run(host, port, debug, **options) def load_config(self): """API settings are loaded from standard python modules. First from @@ -259,14 +254,13 @@ def find_settings_file(file_name): settings_file = os.path.join(abspath, file_name) if os.path.isfile(settings_file): return settings_file - else: - # try to find settings.py in one of the - # paths in sys.path - for p in sys.path: - for root, dirs, files in os.walk(p): - for f in fnmatch.filter(files, file_name): - if os.path.isfile(os.path.join(root, f)): - return os.path.join(root, file_name) + # try to find settings.py in one of the + # paths in sys.path + for p in sys.path: + for root, dirs, files in os.walk(p): + for f in fnmatch.filter(files, file_name): + if os.path.isfile(os.path.join(root, f)): + return os.path.join(root, file_name) # try to load file from environment variable or settings.py pyfile = find_settings_file( @@ -278,7 +272,7 @@ def find_settings_file(file_name): try: self.config.from_pyfile(pyfile) - except: + except Exception: raise # flask-pymongo compatibility @@ -319,7 +313,7 @@ def validate_domain_struct(self): """ try: domain = self.config["DOMAIN"] - except: + except Exception: raise ConfigException("DOMAIN dictionary missing or wrong.") if not isinstance(domain, dict): raise ConfigException("DOMAIN must be a dict.") @@ -1107,4 +1101,4 @@ def __call__(self, environ, start_response): environ["REQUEST_METHOD"] = environ.get( "HTTP_X_HTTP_METHOD_OVERRIDE", environ["REQUEST_METHOD"] ).upper() - return super(Eve, self).__call__(environ, start_response) + return super().__call__(environ, start_response) diff --git a/eve/io/__init__.py b/eve/io/__init__.py index e0f4753d7..0e7b00b66 100644 --- a/eve/io/__init__.py +++ b/eve/io/__init__.py @@ -11,4 +11,4 @@ """ # flake8: noqa -from eve.io.base import DataLayer, ConnectionException +from eve.io.base import ConnectionException, DataLayer diff --git a/eve/io/base.py b/eve/io/base.py index 8aa2e5440..76ba7cee8 100644 --- a/eve/io/base.py +++ b/eve/io/base.py @@ -10,12 +10,13 @@ :license: BSD, see LICENSE for more details. """ import datetime -import simplejson as json from copy import copy -from flask import request, abort -from eve.utils import date_to_str + +import simplejson as json +from flask import abort, request + from eve.auth import auth_field_and_value -from eve.utils import config, auto_fields, debug_error_message +from eve.utils import auto_fields, config, date_to_str, debug_error_message class BaseJSONEncoder(json.JSONEncoder): @@ -27,11 +28,11 @@ def default(self, obj): if isinstance(obj, datetime.datetime): # convert any datetime to RFC 1123 format return date_to_str(obj) - elif isinstance(obj, (datetime.time, datetime.date)): + if isinstance(obj, (datetime.time, datetime.date)): # should not happen since the only supported date-like format # supported at dmain schema level is 'datetime' . return obj.isoformat() - elif isinstance(obj, set): + if isinstance(obj, set): # convert set objects to encodable lists return list(obj) return json.JSONEncoder.default(self, obj) @@ -58,7 +59,7 @@ def __str__(self): return msg -class DataLayer(object): +class DataLayer(): """Base data layer class. Defines the interface that actual data-access classes, being subclasses, must implement. Implemented as a Flask extension. @@ -528,7 +529,7 @@ def _client_projection(self, req): client_projection = json.loads(req.projection) if not isinstance(client_projection, dict): raise Exception("The projection parameter has to be a " "dict") - except: + except Exception: abort( 400, description=debug_error_message( diff --git a/eve/io/media.py b/eve/io/media.py index 82d4436a1..f97f0b956 100644 --- a/eve/io/media.py +++ b/eve/io/media.py @@ -11,7 +11,7 @@ """ -class MediaStorage(object): +class MediaStorage(): """The MediaStorage class provides a standardized API for storing files, along with a set of default behaviors that all other storage systems can inherit or override as necessary. diff --git a/eve/io/mongo/__init__.py b/eve/io/mongo/__init__.py index c96be063f..08e28fef0 100644 --- a/eve/io/mongo/__init__.py +++ b/eve/io/mongo/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ +from eve.io.mongo.media import GridFSMediaStorage # flake8: noqa from eve.io.mongo.mongo import Mongo, MongoJSONEncoder, ensure_mongo_indexes from eve.io.mongo.validation import Validator -from eve.io.mongo.media import GridFSMediaStorage diff --git a/eve/io/mongo/flask_pymongo.py b/eve/io/mongo/flask_pymongo.py index f0ad8a49f..7bba0f2f1 100644 --- a/eve/io/mongo/flask_pymongo.py +++ b/eve/io/mongo/flask_pymongo.py @@ -15,7 +15,7 @@ from pymongo import MongoClient, uri_parser -class PyMongo(object): +class PyMongo(): """ Creates Mongo connection and database based on Flask configuration. """ diff --git a/eve/io/mongo/geo.py b/eve/io/mongo/geo.py index 7b1853663..c2e8e16ed 100644 --- a/eve/io/mongo/geo.py +++ b/eve/io/mongo/geo.py @@ -32,7 +32,7 @@ def _correct_position(self, position): class Geometry(GeoJSON): def __init__(self, json): - super(Geometry, self).__init__(json) + super().__init__(json) try: if ( not isinstance(self["coordinates"], list) @@ -45,7 +45,7 @@ def __init__(self, json): class GeometryCollection(GeoJSON): def __init__(self, json): - super(GeometryCollection, self).__init__(json) + super().__init__(json) try: if not isinstance(self["geometries"], list): raise TypeError @@ -58,14 +58,14 @@ def __init__(self, json): class Point(Geometry): def __init__(self, json): - super(Point, self).__init__(json) + super().__init__(json) if not self._correct_position(self["coordinates"]): raise TypeError class MultiPoint(GeoJSON): def __init__(self, json): - super(MultiPoint, self).__init__(json) + super().__init__(json) for position in self["coordinates"]: if not self._correct_position(position): raise TypeError @@ -73,7 +73,7 @@ def __init__(self, json): class LineString(GeoJSON): def __init__(self, json): - super(LineString, self).__init__(json) + super().__init__(json) for position in self["coordinates"]: if not self._correct_position(position): raise TypeError @@ -81,7 +81,7 @@ def __init__(self, json): class MultiLineString(GeoJSON): def __init__(self, json): - super(MultiLineString, self).__init__(json) + super().__init__(json) for linestring in self["coordinates"]: for position in linestring: if not self._correct_position(position): @@ -90,7 +90,7 @@ def __init__(self, json): class Polygon(GeoJSON): def __init__(self, json): - super(Polygon, self).__init__(json) + super().__init__(json) for linestring in self["coordinates"]: for position in linestring: if not self._correct_position(position): @@ -99,7 +99,7 @@ def __init__(self, json): class MultiPolygon(GeoJSON): def __init__(self, json): - super(MultiPolygon, self).__init__(json) + super().__init__(json) for polygon in self["coordinates"]: for linestring in polygon: for position in linestring: @@ -109,7 +109,7 @@ def __init__(self, json): class Feature(GeoJSON): def __init__(self, json): - super(Feature, self).__init__(json) + super().__init__(json) try: geometry = self["geometry"] factory = factories[geometry["type"]] @@ -121,7 +121,7 @@ def __init__(self, json): class FeatureCollection(GeoJSON): def __init__(self, json): - super(FeatureCollection, self).__init__(json) + super().__init__(json) try: if not isinstance(self["features"], list): raise TypeError diff --git a/eve/io/mongo/media.py b/eve/io/mongo/media.py index 07883a0a0..e7237e4aa 100644 --- a/eve/io/mongo/media.py +++ b/eve/io/mongo/media.py @@ -31,7 +31,7 @@ def __init__(self, app=None): .. versionchanged:: 0.6 Support for multiple, cached, GridFS instances """ - super(GridFSMediaStorage, self).__init__(app) + super().__init__(app) self.validate() self._fs = {} @@ -80,7 +80,7 @@ def get(self, _id, resource=None): _file = None try: _file = self.fs(resource).get(_id) - except: + except Exception: pass return _file diff --git a/eve/io/mongo/mongo.py b/eve/io/mongo/mongo.py index 97d49b8f4..ab44ff802 100644 --- a/eve/io/mongo/mongo.py +++ b/eve/io/mongo/mongo.py @@ -9,34 +9,29 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ +import ast +import decimal import itertools +from collections import OrderedDict +from copy import copy from datetime import datetime -import ast import pymongo import simplejson as json -from bson import ObjectId +from bson import ObjectId, decimal128 from bson.dbref import DBRef -from copy import copy -from flask import abort, request, g -from .flask_pymongo import PyMongo +from flask import abort, g, request from pymongo import WriteConcern from werkzeug.exceptions import HTTPException -import decimal -from bson import decimal128 -from collections import OrderedDict from eve.auth import resource_auth -from eve.io.base import DataLayer, ConnectionException, BaseJSONEncoder -from eve.io.mongo.parser import parse, ParseError -from eve.utils import ( - config, - debug_error_message, - validate_filters, - str_to_date, - str_type, -) +from eve.io.base import BaseJSONEncoder, ConnectionException, DataLayer +from eve.io.mongo.parser import ParseError, parse +from eve.utils import (config, debug_error_message, str_to_date, str_type, + validate_filters) + from ...versioning import versioned_id_field +from .flask_pymongo import PyMongo class MongoJSONEncoder(BaseJSONEncoder): @@ -71,7 +66,7 @@ def default(self, obj): if isinstance(obj, decimal128.Decimal128): return str(obj) # delegate rendering to base class method - return super(MongoJSONEncoder, self).default(obj) + return super().default(obj) class Mongo(DataLayer): @@ -204,7 +199,7 @@ def find(self, resource, req, sub_resource_lookup, perform_count=True): .. versionchanged:: 0.0.4 retrieves the target collection via the new config.SOURCES helper. """ - args = dict() + args = {} if req and req.max_results: args["limit"] = req.max_results @@ -272,7 +267,7 @@ def find(self, resource, req, sub_resource_lookup, perform_count=True): if perform_count: try: count = target.count_documents(spec) - except: + except Exception: # fallback to deprecated method. this might happen when the query # includes operators not supported by count_documents(). one # documented use-case is when we're running on mongo 3.4 and below, @@ -352,8 +347,7 @@ def find_one( return target.with_options(**mongo_options).find_one( filter_, projection or None ) - else: - return target.find_one(filter_, projection or None) + return target.find_one(filter_, projection or None) def find_one_raw(self, resource, **lookup): """Retrieves a single raw document. @@ -733,7 +727,7 @@ def get_value_from_query(self, query, field_name): """ if field_name in query: return query[field_name] - elif "$and" in query: + if "$and" in query: for condition in query["$and"]: if field_name in condition: return condition[field_name] @@ -771,15 +765,14 @@ def is_empty(self, resource): # faster, but we can only afford it if there's now predefined # filter on the datasource. return coll.count_documents({}) == 0 - else: - # fallback on find() since we have a filter to apply. - try: - # need to check if the whole resultset is missing, no - # matter the IMS header. - del filter_[config.LAST_UPDATED] - except: - pass - return coll.count_documents(filter_) == 0 + # fallback on find() since we have a filter to apply. + try: + # need to check if the whole resultset is missing, no + # matter the IMS header. + del filter_[config.LAST_UPDATED] + except Exception: + pass + return coll.count_documents(filter_) == 0 except pymongo.errors.OperationFailure as e: # see comment in :func:`insert()`. self.app.logger.exception(e) @@ -820,7 +813,7 @@ def _mongotize(self, source, resource, parse_objectid=False): def try_cast(k, v, should_parse_objectid): try: return datetime.strptime(v, config.DATE_FORMAT) - except: + except Exception: if k in (id_field, id_field_versioned) or should_parse_objectid: try: # Convert to unicode because ObjectId() interprets @@ -833,7 +826,7 @@ def try_cast(k, v, should_parse_objectid): # We're on Python 3 so it's all unicode already. r = ObjectId(v) return r - except: + except Exception: return v else: return v @@ -858,9 +851,8 @@ def dict_sub_schema(base): possible_types = [get_schema_type(keys, item) for item in items] if "objectid" in possible_types: return "objectid" - else: - return next((t for t in possible_types if t), None) - elif "schema" in schema[k]: + return next((t for t in possible_types if t), None) + if "schema" in schema[k]: # recursively check the schema return get_schema_type(keys, dict_sub_schema(schema[k]["schema"])) elif schema_type == "dict": @@ -975,7 +967,7 @@ def _convert_where_request_to_dict(self, resource, req): except HTTPException: # _sanitize() is raising an HTTP exception; let it fire. raise - except: + except Exception: # couldn't parse as mongo query; give the python parser a shot. try: query = parse(req.where) @@ -1160,7 +1152,7 @@ def _create_index(app, resource, name, list_of_keys, index_options): try: # mongo_prefix might have been set by Auth class instance px = g.get("mongo_prefix") - except: + except Exception: px = app.config["DOMAIN"][resource].get("mongo_prefix", "MONGO") with app.app_context(): diff --git a/eve/io/mongo/parser.py b/eve/io/mongo/parser.py index cbe6fd886..fad0d82f3 100644 --- a/eve/io/mongo/parser.py +++ b/eve/io/mongo/parser.py @@ -14,6 +14,7 @@ import ast import sys from datetime import datetime # noqa + from bson import ObjectId # noqa @@ -68,7 +69,7 @@ def visit_Module(self, node): # if we didn't obtain a query, it is likely that an unsupported # python expression has been passed. - if self.mongo_query == {}: + if not self.mongo_query: raise ParseError( "Only conditional statements with boolean " "(and, or) and comparison operators are " @@ -125,7 +126,7 @@ def visit_Call(self, node): if node.func.id == "ObjectId": try: self.current_value = ObjectId(node.args[0].s) - except: + except Exception: pass elif node.func.id == "datetime": values = [] @@ -133,7 +134,7 @@ def visit_Call(self, node): values.append(arg.n) try: self.current_value = datetime(*values) - except: + except Exception: pass def visit_Attribute(self, node): diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index e733a023d..549cebebd 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -17,17 +17,9 @@ from werkzeug.datastructures import FileStorage from eve.auth import auth_field_and_value -from eve.io.mongo.geo import ( - Point, - MultiPoint, - LineString, - Polygon, - MultiLineString, - MultiPolygon, - GeometryCollection, - Feature, - FeatureCollection, -) +from eve.io.mongo.geo import (Feature, FeatureCollection, GeometryCollection, + LineString, MultiLineString, MultiPoint, + MultiPolygon, Point, Polygon) from eve.utils import config from eve.validation import Validator from eve.versioning import get_data_version_relation_document diff --git a/eve/logging.py b/eve/logging.py index 47ec40025..1a1803157 100644 --- a/eve/logging.py +++ b/eve/logging.py @@ -1,8 +1,8 @@ from __future__ import absolute_import import logging -from flask import request +from flask import request # TODO right now we are only logging exceptions. We should probably # add support for some INFO and maybe DEBUG level logging (like, log each time diff --git a/eve/methods/__init__.py b/eve/methods/__init__.py index 9cf0cd478..eaf7a9cb1 100644 --- a/eve/methods/__init__.py +++ b/eve/methods/__init__.py @@ -10,9 +10,9 @@ :license: BSD, see LICENSE for more details. """ +from eve.methods.delete import delete, deleteitem # flake8: noqa from eve.methods.get import get, getitem -from eve.methods.post import post from eve.methods.patch import patch +from eve.methods.post import post from eve.methods.put import put -from eve.methods.delete import delete, deleteitem diff --git a/eve/methods/common.py b/eve/methods/common.py index 220fc42da..62e890b6a 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -9,9 +9,10 @@ :copyright: (c) 2017 by Nicola Iarocci. :license: BSD, see LICENSE for more details. """ -import re import base64 +import re import time +from collections import Counter from copy import copy from datetime import datetime, timezone from functools import wraps @@ -19,19 +20,16 @@ import simplejson as json from bson.dbref import DBRef from bson.errors import InvalidId -from cerberus import schema_registry, rules_set_registry -from flask import abort, current_app as app, g, request -from werkzeug.datastructures import MultiDict, CombinedMultiDict - -from eve.utils import ( - auto_fields, - config, - debug_error_message, - document_etag, - parse_request, -) -from eve.versioning import get_data_version_relation_document, resolve_document_version -from collections import Counter +from cerberus import rules_set_registry, schema_registry +from flask import abort +from flask import current_app as app +from flask import g, request +from werkzeug.datastructures import CombinedMultiDict, MultiDict + +from eve.utils import (auto_fields, config, debug_error_message, document_etag, + parse_request) +from eve.versioning import (get_data_version_relation_document, + resolve_document_version) def get_document( @@ -152,7 +150,7 @@ def parse(value, resource): try: # assume it's not decoded to json yet (request Content-Type = form) document = json.loads(value) - except: + except Exception: # already a json document = value @@ -161,7 +159,7 @@ def parse(value, resource): # formatted objectid). try: document = serialize(document, resource) - except: + except Exception: pass return document @@ -197,13 +195,13 @@ def payload(): if content_type in config.JSON_REQUEST_CONTENT_TYPES: return request.get_json(force=True) - elif content_type == "application/x-www-form-urlencoded": + if content_type == "application/x-www-form-urlencoded": return ( multidict_to_dict(request.form) if len(request.form) else abort(400, description="No form-urlencoded data supplied") ) - elif content_type == "multipart/form-data": + if content_type == "multipart/form-data": # as multipart is also used for file uploads, we let an empty # request.form go through as long as there are also files in the # request. @@ -226,10 +224,8 @@ def payload(): payload = CombinedMultiDict([formItems, request.files]) return multidict_to_dict(payload) - else: - abort(400, description="No multipart/form-data supplied") - else: - abort(400, description="Unknown or no Content-Type header supplied") + abort(400, description="No multipart/form-data supplied") + abort(400, description="Unknown or no Content-Type header supplied") def multidict_to_dict(multidict): @@ -243,11 +239,10 @@ def multidict_to_dict(multidict): if len(value) == 1: d[key] = value[0] return d - else: - return multidict.to_dict() + return multidict.to_dict() -class RateLimit(object): +class RateLimit(): """Implements the Rate-Limiting logic using Redis as a backend. :param key_prefix: the key used to uniquely identify a client. @@ -349,8 +344,7 @@ def last_updated(document): """ if config.LAST_UPDATED in document: return document[config.LAST_UPDATED].replace(tzinfo=None) - else: - return epoch() + return epoch() def date_created(document): @@ -459,12 +453,12 @@ def resolve_schema(schema): else document[field] ) for subdocument in embedded: - if type(subdocument) is not dict: + if not isinstance(subdocument, dict): # value is not a dict - continue # serialization error will be reported by # validation if appropriate continue - elif "schema" in field_schema: + if "schema" in field_schema: serialize( subdocument, schema=field_schema["schema"] ) @@ -757,7 +751,7 @@ def resolve_data_relation_links(document, resource): if ( field in document and document[field] is not None - and document[field] is not [] + and document[field] != [] ): related_links = [] @@ -801,7 +795,7 @@ def resolve_data_relation_links(document, resource): else: related_dict.update({field: related_links[0]}) - if related_dict != {}: + if related_dict: document[config.LINKS].update({"related": related_dict}) @@ -936,10 +930,9 @@ def embedded_document(references, data_relation, field_name): if output_is_list: return embedded_docs - elif embedded_docs: + if embedded_docs: return embedded_docs[0] - else: - return None + return None def sort_db_response(embedded_docs, id_value_to_sort, list_of_id_field_name): @@ -1176,10 +1169,8 @@ def resolve_one_media(file_id, resource): ) return ret - else: - return ret_file - else: - return None + return ret_file + return None def marshal_write_response(document, resource): @@ -1206,7 +1197,7 @@ def marshal_write_response(document, resource): if auth_field and auth_field not in resource_def["schema"]: try: del document[auth_field] - except: + except Exception: # 'auth_field' value has not been set by the auth class. pass return document @@ -1444,8 +1435,7 @@ def strip_prefix(hit): # We are creating a path for data relation resources if resource and not re.search(config.DOMAIN[resource]["url"], path): return config.DOMAIN[resource]["url"] - else: - return path + return path def oplog_push(resource, document, op, id=None): diff --git a/eve/methods/delete.py b/eve/methods/delete.py index b906e8838..a595d7d63 100644 --- a/eve/methods/delete.py +++ b/eve/methods/delete.py @@ -10,25 +10,18 @@ :license: BSD, see LICENSE for more details. """ -from flask import current_app as app, abort -from eve.utils import config, ParsedRequest -from eve.auth import requires_auth -from eve.methods.common import ( - get_document, - ratelimit, - pre_event, - oplog_push, - resolve_document_etag, - utcnow, -) -from eve.versioning import ( - versioned_id_field, - resolve_document_version, - insert_versioning_documents, - late_versioning_catch, -) import copy +from flask import abort +from flask import current_app as app + +from eve.auth import requires_auth +from eve.methods.common import (get_document, oplog_push, pre_event, ratelimit, + resolve_document_etag, utcnow) +from eve.utils import ParsedRequest, config +from eve.versioning import (insert_versioning_documents, late_versioning_catch, + resolve_document_version, versioned_id_field) + def all_done(): return {}, None, None, 204 diff --git a/eve/methods/get.py b/eve/methods/get.py index 37dc0b1e0..c380e6180 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -12,31 +12,23 @@ """ from __future__ import division +import copy import math -import copy import simplejson as json -from flask import current_app as app, abort, request +from flask import abort +from flask import current_app as app +from flask import request from werkzeug.datastructures import MultiDict -from .common import ( - ratelimit, - epoch, - pre_event, - resolve_embedded_fields, - build_response_document, - resource_link, - document_link, - last_updated, -) from eve.auth import requires_auth -from eve.utils import parse_request, home_link, querydef, config -from eve.versioning import ( - synthesize_versioned_document, - versioned_id_field, - get_old_document, - diff_document, -) +from eve.utils import config, home_link, parse_request, querydef +from eve.versioning import (diff_document, get_old_document, + synthesize_versioned_document, versioned_id_field) + +from .common import (build_response_document, document_link, epoch, + last_updated, pre_event, ratelimit, + resolve_embedded_fields, resource_link) @ratelimit() @@ -123,8 +115,7 @@ def get_internal(resource, **lookup): return _perform_aggregation( resource, aggregation["pipeline"], aggregation["options"] ) - else: - return _perform_find(resource, lookup) + return _perform_find(resource, lookup) def _perform_aggregation(resource, pipeline, options): diff --git a/eve/methods/patch.py b/eve/methods/patch.py index 61851eae4..9b3ae1c8e 100644 --- a/eve/methods/patch.py +++ b/eve/methods/patch.py @@ -11,30 +11,22 @@ """ from copy import deepcopy -from flask import current_app as app, abort + +from cerberus.validator import DocumentError +from flask import abort +from flask import current_app as app from werkzeug import exceptions -from eve.utils import config, debug_error_message, parse_request + from eve.auth import requires_auth -from cerberus.validator import DocumentError -from eve.methods.common import ( - get_document, - parse, - payload as payload_, - ratelimit, - pre_event, - store_media_files, - resolve_embedded_fields, - build_response_document, - marshal_write_response, - resolve_document_etag, - oplog_push, - utcnow, -) -from eve.versioning import ( - resolve_document_version, - insert_versioning_documents, - late_versioning_catch, -) +from eve.methods.common import (build_response_document, get_document, + marshal_write_response, oplog_push, parse) +from eve.methods.common import payload as payload_ +from eve.methods.common import (pre_event, ratelimit, resolve_document_etag, + resolve_embedded_fields, store_media_files, + utcnow) +from eve.utils import config, debug_error_message, parse_request +from eve.versioning import (insert_versioning_documents, late_versioning_catch, + resolve_document_version) @ratelimit() diff --git a/eve/methods/post.py b/eve/methods/post.py index 93e3b47e2..3a6869767 100644 --- a/eve/methods/post.py +++ b/eve/methods/post.py @@ -11,27 +11,21 @@ :license: BSD, see LICENSE for more details. """ -from flask import current_app as app, abort -from eve.utils import config, parse_request, debug_error_message -from eve.auth import requires_auth from cerberus.validator import DocumentError -from eve.methods.common import ( - parse, - payload, - ratelimit, - pre_event, - store_media_files, - resolve_user_restricted_access, - resolve_embedded_fields, - build_response_document, - marshal_write_response, - resolve_sub_resource_path, - resolve_document_etag, - oplog_push, - resource_link, - utcnow, -) -from eve.versioning import resolve_document_version, insert_versioning_documents +from flask import abort +from flask import current_app as app + +from eve.auth import requires_auth +from eve.methods.common import (build_response_document, + marshal_write_response, oplog_push, parse, + payload, pre_event, ratelimit, + resolve_document_etag, resolve_embedded_fields, + resolve_sub_resource_path, + resolve_user_restricted_access, resource_link, + store_media_files, utcnow) +from eve.utils import config, debug_error_message, parse_request +from eve.versioning import (insert_versioning_documents, + resolve_document_version) @ratelimit() diff --git a/eve/methods/put.py b/eve/methods/put.py index 1810a0197..448c0a054 100644 --- a/eve/methods/put.py +++ b/eve/methods/put.py @@ -10,34 +10,24 @@ :license: BSD, see LICENSE for more details. """ -from flask import current_app as app, abort +from cerberus.validator import DocumentError +from flask import abort +from flask import current_app as app from werkzeug import exceptions from eve.auth import auth_field_and_value, requires_auth -from eve.methods.common import ( - get_document, - parse, - payload as payload_, - ratelimit, - pre_event, - store_media_files, - resolve_user_restricted_access, - resolve_embedded_fields, - build_response_document, - marshal_write_response, - resolve_sub_resource_path, - resolve_document_etag, - oplog_push, - utcnow, -) +from eve.methods.common import (build_response_document, get_document, + marshal_write_response, oplog_push, parse) +from eve.methods.common import payload as payload_ +from eve.methods.common import (pre_event, ratelimit, resolve_document_etag, + resolve_embedded_fields, + resolve_sub_resource_path, + resolve_user_restricted_access, + store_media_files, utcnow) from eve.methods.post import post_internal from eve.utils import config, debug_error_message, parse_request -from cerberus.validator import DocumentError -from eve.versioning import ( - resolve_document_version, - insert_versioning_documents, - late_versioning_catch, -) +from eve.versioning import (insert_versioning_documents, late_versioning_catch, + resolve_document_version) @ratelimit() @@ -151,8 +141,7 @@ def put_internal( id = str(id) payload[resource_def["id_field"]] = id return post_internal(resource, payl=payload) - else: - abort(404) + abort(404) # If the document exists, but is owned by someone else, return # 403 Forbidden diff --git a/eve/render.py b/eve/render.py index b542c2592..da7bf2e1d 100644 --- a/eve/render.py +++ b/eve/render.py @@ -10,23 +10,22 @@ :license: BSD, see LICENSE for more details. """ +import datetime import re import time -import datetime +from collections import OrderedDict # noqa +from functools import wraps + import simplejson as json -from werkzeug import utils +from flask import Response, abort +from flask import current_app as app +from flask import make_response, request from markupsafe import escape -from functools import wraps +from werkzeug import utils + from eve.methods.common import get_rate_limit -from eve.utils import ( - date_to_str, - date_to_rfc1123, - config, - debug_error_message, - import_from_string, -) -from flask import make_response, request, Response, current_app as app, abort -from collections import OrderedDict # noqa +from eve.utils import (config, date_to_rfc1123, date_to_str, + debug_error_message, import_from_string) def raise_event(f): @@ -89,8 +88,7 @@ def send_response(resource, response): """ if isinstance(response, Response): return response - else: - return _prepare_response(resource, *response if response else [None]) + return _prepare_response(resource, *response if response else [None]) def _prepare_response( @@ -289,7 +287,7 @@ def _best_mime(): return best_match, renders[best_match] -class Renderer(object): +class Renderer(): """Base class for all the renderers. Renderer should set valid `mime` attr and have `.render()` method implemented. @@ -464,7 +462,7 @@ def xml_add_items(cls, data): """ try: xml = "".join(cls.xml_item(item) for item in data[config.ITEMS]) - except: + except Exception: xml = cls.xml_dict(data) return xml @@ -547,14 +545,12 @@ def xml_field_open(cls, field, idx, related_links): escape(related_links[field][idx]["href"]), related_links[field][idx]["title"], ) - else: - return '<%s href="%s" title="%s">' % ( - field, - escape(related_links[field]["href"]), - related_links[field]["title"], - ) - else: - return "<%s>" % field + return '<%s href="%s" title="%s">' % ( + field, + escape(related_links[field]["href"]), + related_links[field]["title"], + ) + return "<%s>" % field @classmethod def xml_field_close(cls, field): diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 1d6543ff5..8eb50975b 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -1,23 +1,20 @@ # -*- coding: utf-8 -*- -import unittest -import eve -import string -import random import os -import simplejson as json +import random +import string +import unittest from datetime import datetime, timedelta, timezone + +import simplejson as json from bson import ObjectId from pymongo import MongoClient -from eve.tests.test_settings import ( - MONGO_PASSWORD, - MONGO_USERNAME, - MONGO_DBNAME, - DOMAIN, - MONGO_HOST, - MONGO_PORT, -) -from eve import ISSUES, ETAG + +import eve +from eve import ETAG, ISSUES +from eve.tests.test_settings import (DOMAIN, MONGO_DBNAME, MONGO_HOST, + MONGO_PASSWORD, MONGO_PORT, + MONGO_USERNAME) from eve.utils import date_to_str try: @@ -26,7 +23,7 @@ from urllib.parse import parse_qs, urlparse -class ValueStack(object): +class ValueStack(): """ Descriptor to store multiple assignments in an attribute. @@ -385,7 +382,7 @@ def dropDB(self): class TestBase(TestMinimal): def setUp(self, url_converters=None): - super(TestBase, self).setUp(url_converters=url_converters) + super().setUp(url_converters=url_converters) self.disabled_bulk = "disabled_bulk" self.disabled_bulk_url = "/%s" % self.domain[self.disabled_bulk]["url"] @@ -488,8 +485,7 @@ def setUp(self, url_converters=None): def response_item(self, response, i=0): if self.app.config["HATEOAS"]: return response["_items"][i] - else: - return response[i] + return response[i] def random_contacts(self, num, standard_date_fields=True): schema = DOMAIN["contacts"]["schema"] diff --git a/eve/tests/auth.py b/eve/tests/auth.py index fbd3ae76c..d2846e8e6 100644 --- a/eve/tests/auth.py +++ b/eve/tests/auth.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -import simplejson as json +from io import BytesIO +import simplejson as json from bson import ObjectId import eve from eve import Eve -from eve.auth import BasicAuth, TokenAuth, HMACAuth +from eve.auth import BasicAuth, HMACAuth, TokenAuth from eve.tests import TestBase from eve.tests.test_settings import MONGO_DBNAME -from io import BytesIO class ValidBasicAuth(BasicAuth): def __init__(self): self.request_auth_value = "admin" - super(ValidBasicAuth, self).__init__() + super().__init__() def check_auth(self, username, password, allowed_roles, resource, method): self.set_request_auth_value(self.request_auth_value) @@ -58,7 +58,7 @@ class BadHMACAuth(HMACAuth): class TestBasicAuth(TestBase): def setUp(self): - super(TestBasicAuth, self).setUp() + super().setUp() self.app = Eve(settings=self.settings_file, auth=ValidBasicAuth) self.test_client = self.app.test_client() self.content_type = ("Content-Type", "application/json") @@ -305,7 +305,7 @@ def test_ALLOWED_ROLES_does_not_change(self): class TestTokenAuth(TestBasicAuth): def setUp(self): - super(TestTokenAuth, self).setUp() + super().setUp() self.app = Eve(settings=self.settings_file, auth=ValidTokenAuth) self.test_client = self.app.test_client() self.valid_auth = [ @@ -324,7 +324,7 @@ def test_custom_auth(self): class TestBearerTokenAuth(TestTokenAuth): def setUp(self): - super(TestBearerTokenAuth, self).setUp() + super().setUp() self.valid_auth = [("Authorization", "Token test_token"), self.content_type] self.valid_media_auth = [ ("Authorization", "Token test_token"), @@ -341,7 +341,7 @@ def test_bad_auth_class(self): class TestCustomTokenAuth(TestTokenAuth): def setUp(self): - super(TestCustomTokenAuth, self).setUp() + super().setUp() self.valid_auth = [("Authorization", "Token test_token"), self.content_type] self.valid_media_auth = [ ("Authorization", "Token test_token"), @@ -358,7 +358,7 @@ def test_bad_auth_class(self): class TestHMACAuth(TestBasicAuth): def setUp(self): - super(TestHMACAuth, self).setUp() + super().setUp() self.app = Eve(settings=self.settings_file, auth=ValidHMACAuth) self.test_client = self.app.test_client() self.valid_auth = [("Authorization", "admin:secret"), self.content_type] @@ -446,7 +446,7 @@ def test_resource_only_auth(self): class TestUserRestrictedAccess(TestBase): def setUp(self): - super(TestUserRestrictedAccess, self).setUp() + super().setUp() self.app = Eve(settings=self.settings_file, auth=ValidBasicAuth) diff --git a/eve/tests/config.py b/eve/tests/config.py index 9d4d08aea..e29d66bd2 100644 --- a/eve/tests/config.py +++ b/eve/tests/config.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -import eve import os -from eve.flaskapp import RegexConverter -from eve.flaskapp import Eve + +import eve +from eve.exceptions import ConfigException, SchemaException +from eve.flaskapp import Eve, RegexConverter from eve.io.base import DataLayer +from eve.io.mongo import Mongo, Validator from eve.tests import TestBase from eve.tests.test_settings import MONGO_HOST, MONGO_PORT -from eve.exceptions import ConfigException, SchemaException -from eve.io.mongo import Mongo, Validator class TestConfig(TestBase): @@ -299,7 +299,7 @@ def _test_datasource_for_resource(self, resource): ) self.assertEqual( - datasource["projection"], dict((field, 1) for (field) in compare) + datasource["projection"], dict((field, 1) for field in compare) ) self.assertEqual(datasource["source"], resource) self.assertEqual(datasource["filter"], None) diff --git a/eve/tests/endpoints.py b/eve/tests/endpoints.py index 4ed26628b..a4ccef7f9 100644 --- a/eve/tests/endpoints.py +++ b/eve/tests/endpoints.py @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- +import os +from datetime import datetime +from uuid import UUID + import pytest import simplejson as json from werkzeug.routing import BaseConverter -from eve.tests import TestBase, TestMinimal + from eve import Eve -from datetime import datetime -from eve.utils import config from eve.io.base import BaseJSONEncoder -from eve.tests.test_settings import MONGO_DBNAME, MONGO_USERNAME, MONGO_PASSWORD -from uuid import UUID from eve.io.mongo import Validator -import os +from eve.tests import TestBase, TestMinimal +from eve.tests.test_settings import (MONGO_DBNAME, MONGO_PASSWORD, + MONGO_USERNAME) +from eve.utils import config class UUIDEncoder(BaseJSONEncoder): @@ -22,9 +25,8 @@ class UUIDEncoder(BaseJSONEncoder): def default(self, obj): if isinstance(obj, UUID): return str(obj) - else: - # delegate rendering to base class method - return super(UUIDEncoder, self).default(obj) + # delegate rendering to base class method + return super().default(obj) class UUIDConverter(BaseConverter): @@ -33,7 +35,7 @@ class UUIDConverter(BaseConverter): """ def __init__(self, url_map, strict=True): - super(UUIDConverter, self).__init__(url_map) + super().__init__(url_map) def to_python(self, value): return UUID(value) @@ -78,7 +80,7 @@ def setUp(self): self.url = "/uuids/%s" % self.uuid_valid self.headers = [("Content-Type", "application/json")] - super(TestCustomConverters, self).setUp( + super().setUp( settings_file=settings, url_converters=url_converters ) @@ -353,7 +355,7 @@ def test_schema_endpoint(self): def test_schema_endpoint_does_not_attempt_callable_serialization(self): self.domain[self.known_resource]["schema"]["lambda"] = { "type": "boolean", - "coerce": lambda v: v if type(v) is bool else v.lower() in ["true", "1"], + "coerce": lambda v: v if isinstance(v, bool) else v.lower() in ["true", "1"], } known_schema_path = "/schema/%s" % self.known_resource self.app.config["SCHEMA_ENDPOINT"] = "schema" diff --git a/eve/tests/methods/common.py b/eve/tests/methods/common.py index 7164caa97..5ad11bb53 100644 --- a/eve/tests/methods/common.py +++ b/eve/tests/methods/common.py @@ -1,18 +1,20 @@ import time +from collections import OrderedDict # noqa from datetime import datetime from random import shuffle + import simplejson as json from bson import ObjectId, decimal128 from bson.dbref import DBRef -from eve.tests.suite_generator import EmbeddedDoc -from eve.methods.common import serialize, normalize_dotted_fields, sort_per_resource + +from eve.methods.common import (normalize_dotted_fields, serialize, + sort_per_resource) from eve.tests import TestBase -from eve.tests.auth import ValidBasicAuth, ValidTokenAuth, ValidHMACAuth +from eve.tests.auth import ValidBasicAuth, ValidHMACAuth, ValidTokenAuth +from eve.tests.suite_generator import EmbeddedDoc from eve.tests.test_settings import MONGO_DBNAME from eve.utils import config -from collections import OrderedDict # noqa - class TestSerializer(TestBase): def test_serialize_array_of_tipes(self): @@ -472,7 +474,7 @@ def compare_recursive(a, b): class TestOpLogBase(TestBase): def setUp(self): - super(TestOpLogBase, self).setUp() + super().setUp() self.test_field, self.test_value = "ref", "1234567890123456789054321" self.data = {self.test_field: self.test_value} self.test_client = self.app.test_client() @@ -511,7 +513,7 @@ def assertOpLogEntry(self, entry, op, user=None): class TestOpLogEndpointDisabled(TestOpLogBase): def setUp(self): - super(TestOpLogEndpointDisabled, self).setUp() + super().setUp() self.app.config["OPLOG"] = True from eve.default_settings import OPLOG_CHANGE_METHODS @@ -539,7 +541,7 @@ def test_post_oplog(self): class TestOpLogEndpointEnabled(TestOpLogBase): def setUp(self): - super(TestOpLogEndpointEnabled, self).setUp() + super().setUp() self.app.config["OPLOG"] = True self.app.config["OPLOG_ENDPOINT"] = "oplog" @@ -763,7 +765,7 @@ def test_ticket_681(self): class TestEmbeddedDocuments(TestBase): def setUp(self, url_converters=None): - super(TestEmbeddedDocuments, self).setUp() + super().setUp() def test_sort_per_resource_embedded_docs(self): object_ids = [ObjectId() for _ in range(8)] diff --git a/eve/tests/methods/delete.py b/eve/tests/methods/delete.py index 46cddc2c1..0fd84872d 100644 --- a/eve/tests/methods/delete.py +++ b/eve/tests/methods/delete.py @@ -1,7 +1,8 @@ import copy -import simplejson as json +import simplejson as json from bson import ObjectId + from eve import ETAG from eve.methods.delete import deleteitem_internal from eve.tests import TestBase @@ -12,7 +13,7 @@ class TestDelete(TestBase): def setUp(self): - super(TestDelete, self).setUp() + super().setUp() # Etag used to delete an item (a contact) self.etag_headers = [("If-Match", self.item_etag)] @@ -256,7 +257,7 @@ def delete(self, url, headers=None): class TestSoftDelete(TestDelete): def setUp(self): - super(TestSoftDelete, self).setUp() + super().setUp() # Enable soft delete self.app.config["SOFT_DELETE"] = True @@ -321,7 +322,7 @@ def test_delete_from_resource_endpoint(self): """ # TestDelete deletes resource at known_resource_url, and confirms # subsequent queries to the resource return zero items - super(TestSoftDelete, self).test_delete_from_resource_endpoint() + super().test_delete_from_resource_endpoint() r = self.test_client.get(self.item_id_url) data, status = self.parse_response(r) @@ -667,7 +668,7 @@ def test_exclude_soft_deleted_documents_from_unique_checks(self): class TestResourceSpecificSoftDelete(TestBase): def setUp(self): - super(TestResourceSpecificSoftDelete, self).setUp() + super().setUp() # Enable soft delete for one resource domain = copy.copy(self.domain) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index d3311e5ac..0fdac0f39 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -1,17 +1,19 @@ import base64 import time +from datetime import datetime, timedelta from io import BytesIO + import simplejson as json -from datetime import datetime, timedelta from bson import ObjectId from bson.dbref import DBRef from bson.son import SON from werkzeug.datastructures import ImmutableMultiDict, MultiDict + +from eve.methods.get import get_internal, getitem_internal from eve.tests import TestBase -from eve.tests.utils import DummyEvent from eve.tests.test_settings import MONGO_DBNAME -from eve.utils import str_to_date, date_to_rfc1123 -from eve.methods.get import get_internal, getitem_internal +from eve.tests.utils import DummyEvent +from eve.utils import date_to_rfc1123, str_to_date class TestGet(TestBase): @@ -2208,7 +2210,7 @@ def assertHead(self, url): class TestEvents(TestBase): def setUp(self): - super(TestEvents, self).setUp() + super().setUp() self.devent = DummyEvent(lambda: True) def test_on_pre_GET_for_item(self): diff --git a/eve/tests/methods/patch.py b/eve/tests/methods/patch.py index 7f27e971d..8998d32df 100644 --- a/eve/tests/methods/patch.py +++ b/eve/tests/methods/patch.py @@ -1,13 +1,8 @@ import simplejson as json - from bson import ObjectId from pymongo import ReadPreference -from eve import ETAG -from eve import ISSUES -from eve import LAST_UPDATED -from eve import STATUS -from eve import STATUS_OK +from eve import ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK from eve.methods.patch import patch_internal from eve.tests import TestBase from eve.tests.test_settings import MONGO_DBNAME @@ -388,8 +383,7 @@ def compare_patch_with_get(self, fields, patch_response): ) if isinstance(fields, str): return r[fields] - else: - return [r[field] for field in fields] + return [r[field] for field in fields] def test_patch_allow_unknown(self): changes = {"unknown": "unknown"} diff --git a/eve/tests/methods/patch_atomic_concurrency.py b/eve/tests/methods/patch_atomic_concurrency.py index 7943239c3..944560183 100644 --- a/eve/tests/methods/patch_atomic_concurrency.py +++ b/eve/tests/methods/patch_atomic_concurrency.py @@ -1,5 +1,7 @@ -import simplejson as json import sys + +import simplejson as json + import eve.methods.common from eve.tests import TestBase from eve.utils import config @@ -49,7 +51,7 @@ def setUp(self): sys.modules[ "eve.methods.patch" ].get_document = get_document_simulate_concurrent_update - return super(TestPatchAtomicConcurrent, self).setUp() + return super().setUp() def test_etag_changed_after_get_document(self): """ @@ -65,7 +67,7 @@ def test_etag_changed_after_get_document(self): def tearDown(self): """Remove patch of eve.methods.patch.get_document""" sys.modules["eve.methods.patch"].get_document = self.original_get_document - return super(TestPatchAtomicConcurrent, self).tearDown() + return super().tearDown() def patch(self, url, data, headers=[]): headers.append(("Content-Type", "application/json")) diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index fcc0411d3..0b4c6cf89 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1,21 +1,17 @@ from base64 import b64decode -from bson import ObjectId +from io import BytesIO import simplejson as json +from bson import ObjectId +from werkzeug.datastructures import MultiDict +from eve import DATE_CREATED, ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK +from eve.methods.post import post, post_internal from eve.tests import TestBase -from eve.tests.utils import DummyEvent from eve.tests.test_settings import MONGO_DBNAME - -from eve import STATUS_OK, LAST_UPDATED, DATE_CREATED, ISSUES, STATUS, ETAG -from eve.methods.post import post -from eve.methods.post import post_internal +from eve.tests.utils import DummyEvent from eve.utils import str_type -from io import BytesIO - -from werkzeug.datastructures import MultiDict - class TestPost(TestBase): def test_unknown_resource(self): @@ -1137,8 +1133,7 @@ def compare_post_with_get(self, item_id, fields): self.assertEqual(item[DATE_CREATED], item[LAST_UPDATED]) if isinstance(fields, list): return [item[field] for field in fields] - else: - return item[fields] + return item[fields] def post(self, url, data, headers=None, content_type="application/json"): if not headers: diff --git a/eve/tests/methods/put.py b/eve/tests/methods/put.py index eed6493d0..afb8d1b17 100644 --- a/eve/tests/methods/put.py +++ b/eve/tests/methods/put.py @@ -1,12 +1,8 @@ import simplejson as json - from bson import ObjectId from bson.dbref import DBRef -from eve import ETAG -from eve import ISSUES -from eve import LAST_UPDATED -from eve import STATUS -from eve import STATUS_OK + +from eve import ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK from eve.methods.put import put_internal from eve.tests import TestBase from eve.tests.test_settings import MONGO_DBNAME @@ -585,8 +581,7 @@ def compare_put_with_get(self, fields, put_response): self.assertEqual(raw_r.headers.get("ETag").replace('"', ""), put_response[ETAG]) if isinstance(fields, str): return r[fields] - else: - return [r[field] for field in fields] + return [r[field] for field in fields] class TestEvents(TestBase): diff --git a/eve/tests/methods/ratelimit.py b/eve/tests/methods/ratelimit.py index 79dbf5b0d..85cc91057 100644 --- a/eve/tests/methods/ratelimit.py +++ b/eve/tests/methods/ratelimit.py @@ -1,12 +1,13 @@ -from eve.tests import TestBase import time +from eve.tests import TestBase + class TestRateLimit(TestBase): def setUp(self): - super(TestRateLimit, self).setUp() + super().setUp() try: - from redis import Redis, ConnectionError + from redis import ConnectionError, Redis self.app.redis = Redis() try: diff --git a/eve/tests/renders.py b/eve/tests/renders.py index c39de6737..57879e468 100644 --- a/eve/tests/renders.py +++ b/eve/tests/renders.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- +import simplejson as json from bson import ObjectId + from eve.tests import TestBase -from eve.utils import api_prefix from eve.tests.test_settings import MONGO_DBNAME -import simplejson as json +from eve.utils import api_prefix class TestRenders(TestBase): diff --git a/eve/tests/response.py b/eve/tests/response.py index 6ba1db3f5..5e6a527ab 100644 --- a/eve/tests/response.py +++ b/eve/tests/response.py @@ -1,22 +1,24 @@ # -*- coding: utf-8 -*- +import os from ast import literal_eval -from eve.tests import TestBase + import simplejson as json + import eve -import os +from eve.tests import TestBase class TestResponse(TestBase): def setUp(self): - super(TestResponse, self).setUp() + super().setUp() self.r = self.test_client.get("/%s/" % self.empty_resource) def test_response_data(self): response = None try: response = literal_eval(self.r.get_data().decode()) - except: + except Exception: self.fail("standard response cannot be converted to a dict") self.assertTrue(isinstance(response, dict)) @@ -43,7 +45,7 @@ def test_response_pretty(self): class TestNoHateoas(TestBase): def setUp(self): - super(TestNoHateoas, self).setUp() + super().setUp() self.app.config["HATEOAS"] = False self.domain[self.known_resource]["hateoas"] = False diff --git a/eve/tests/test_io/flask_pymongo.py b/eve/tests/test_io/flask_pymongo.py index d120dffda..710c8e78f 100644 --- a/eve/tests/test_io/flask_pymongo.py +++ b/eve/tests/test_io/flask_pymongo.py @@ -1,21 +1,16 @@ import pytest - -from eve.tests import TestBase from pymongo import MongoClient from pymongo.errors import OperationFailure -from eve.tests.test_settings import ( - MONGO1_DBNAME, - MONGO1_USERNAME, - MONGO1_PASSWORD, - MONGO_HOST, - MONGO_PORT, -) + from eve.io.mongo.flask_pymongo import PyMongo +from eve.tests import TestBase +from eve.tests.test_settings import (MONGO1_DBNAME, MONGO1_PASSWORD, + MONGO1_USERNAME, MONGO_HOST, MONGO_PORT) class TestPyMongo(TestBase): def setUp(self, url_converters=None): - super(TestPyMongo, self).setUp(url_converters) + super().setUp(url_converters) self._setupdb() schema = {"title": {"type": "string"}} settings = {"schema": schema, "mongo_prefix": "MONGO1"} diff --git a/eve/tests/test_io/media.py b/eve/tests/test_io/media.py index 6c9e5aa8f..88f05f052 100644 --- a/eve/tests/test_io/media.py +++ b/eve/tests/test_io/media.py @@ -1,11 +1,13 @@ +import base64 from io import BytesIO from unittest import TestCase + +from bson import ObjectId + +from eve import ETAG, ISSUES, STATUS, STATUS_ERR, STATUS_OK from eve.io.media import MediaStorage from eve.io.mongo import GridFSMediaStorage -from eve.tests import TestBase, MONGO_DBNAME -from eve import STATUS_OK, STATUS, STATUS_ERR, ISSUES, ETAG -import base64 -from bson import ObjectId +from eve.tests import MONGO_DBNAME, TestBase class TestMediaStorage(TestCase): @@ -24,7 +26,7 @@ def test_base_media_storage(self): class TestGridFSMediaStorage(TestBase): def setUp(self): - super(TestGridFSMediaStorage, self).setUp() + super().setUp() self.url = self.known_resource_url self.resource = self.known_resource self.headers = [("Content-Type", "multipart/form-data")] diff --git a/eve/tests/test_io/mongo.py b/eve/tests/test_io/mongo.py index 3ac51c2f4..45f8e03ca 100644 --- a/eve/tests/test_io/mongo.py +++ b/eve/tests/test_io/mongo.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- from datetime import datetime +from unittest import TestCase import simplejson as json from bson import ObjectId, decimal128 from bson.dbref import DBRef from cerberus import SchemaError -from unittest import TestCase -from eve.io.mongo import Validator, Mongo, MongoJSONEncoder -from eve.io.mongo.parser import parse, ParseError +from eve.io.mongo import Mongo, MongoJSONEncoder, Validator +from eve.io.mongo.parser import ParseError, parse from eve.tests import TestBase from eve.tests.test_settings import MONGO_DBNAME diff --git a/eve/tests/test_io/multi_mongo.py b/eve/tests/test_io/multi_mongo.py index 2d500a010..4fcf3caba 100644 --- a/eve/tests/test_io/multi_mongo.py +++ b/eve/tests/test_io/multi_mongo.py @@ -10,19 +10,14 @@ import eve from eve.auth import BasicAuth from eve.tests import TestBase -from eve.tests.test_settings import ( - MONGO1_PASSWORD, - MONGO1_USERNAME, - MONGO1_DBNAME, - MONGO_DBNAME, - MONGO_HOST, - MONGO_PORT, -) +from eve.tests.test_settings import (MONGO1_DBNAME, MONGO1_PASSWORD, + MONGO1_USERNAME, MONGO_DBNAME, MONGO_HOST, + MONGO_PORT) class TestMultiMongo(TestBase): def setUp(self): - super(TestMultiMongo, self).setUp() + super().setUp() self.setupDB2() @@ -32,7 +27,7 @@ def setUp(self): self.app.register_resource("works", settings) def tearDown(self): - super(TestMultiMongo, self).tearDown() + super().tearDown() self.dropDB2() def setupDB2(self): diff --git a/eve/tests/test_logging.py b/eve/tests/test_logging.py index 8ca76a48c..47b82613e 100644 --- a/eve/tests/test_logging.py +++ b/eve/tests/test_logging.py @@ -1,6 +1,7 @@ -from eve.tests import TestBase from testfixtures import log_capture +from eve.tests import TestBase + class TestUtils(TestBase): """collection, document and home_link methods (and resource_uri, which is diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index 84d462cc4..8adee363f 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import copy - MONGO_HOST = "localhost" MONGO_PORT = 27017 MONGO_USERNAME = MONGO1_USERNAME = "test_user" diff --git a/eve/tests/utils.py b/eve/tests/utils.py index cf06d631a..3c8858f26 100644 --- a/eve/tests/utils.py +++ b/eve/tests/utils.py @@ -2,22 +2,14 @@ import copy import hashlib -from bson.json_util import dumps from datetime import datetime, timedelta + +from bson.json_util import dumps + from eve.tests import TestBase -from eve.utils import ( - parse_request, - str_to_date, - config, - weak_date, - date_to_str, - querydef, - document_etag, - extract_key_values, - debug_error_message, - validate_filters, - import_from_string, -) +from eve.utils import (config, date_to_str, debug_error_message, document_etag, + extract_key_values, import_from_string, parse_request, + querydef, str_to_date, validate_filters, weak_date) class TestUtils(TestBase): @@ -27,7 +19,7 @@ class TestUtils(TestBase): """ def setUp(self): - super(TestUtils, self).setUp() + super().setUp() self.dt_fmt = config.DATE_FORMAT self.datestr = "Tue, 18 Sep 2012 10:12:30 GMT" self.valid = datetime.strptime(self.datestr, self.dt_fmt) @@ -300,7 +292,7 @@ def test_import_from_string(self): self.assertEqual(dt, datetime) -class DummyEvent(object): +class DummyEvent(): """ Even handler that records the call parameters and asserts a check diff --git a/eve/tests/versioning.py b/eve/tests/versioning.py index 6f452ce97..150bd62a7 100644 --- a/eve/tests/versioning.py +++ b/eve/tests/versioning.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- -from bson import ObjectId import copy import time + +from bson import ObjectId + +from eve import ETAG, STATUS, STATUS_OK from eve.tests import TestBase -from eve.tests.utils import DummyEvent -from eve import STATUS, STATUS_OK, ETAG from eve.tests.test_settings import MONGO_DBNAME +from eve.tests.utils import DummyEvent class TestVersioningBase(TestBase): @@ -15,7 +17,7 @@ def setUp(self): self.unversioned_field = "prog" self.fields = [self.versioned_field, self.unversioned_field] - super(TestVersioningBase, self).setUp() + super().setUp() self.id_field = self.domain[self.known_resource]["id_field"] self.version_field = self.app.config["VERSION"] @@ -26,7 +28,7 @@ def setUp(self): self._db = self.connection[MONGO_DBNAME] def tearDown(self): - super(TestVersioningBase, self).tearDown() + super().tearDown() self.connection.close() def enableVersioning(self, partial=False): @@ -123,7 +125,7 @@ def assertGoodPutPatch(self, response, status): class TestNormalVersioning(TestVersioningBase): def setUp(self): - super(TestNormalVersioning, self).setUp() + super().setUp() # create some dummy contacts to use for versioning tests self.item = { @@ -301,7 +303,7 @@ def do_test_version_control_the_unkown(self): class TestCompleteVersioning(TestNormalVersioning): def setUp(self): - super(TestCompleteVersioning, self).setUp() + super().setUp() self.enableVersioning() self.insertTestData() @@ -948,7 +950,7 @@ def test_softdelete_version_db_fields(self): class TestVersionedDataRelation(TestNormalVersioning): def setUp(self): - super(TestVersionedDataRelation, self).setUp() + super().setUp() # enable versioning in the invoice data_relation definition self.enableDataVersionRelation() @@ -1152,7 +1154,7 @@ def test_softdelete_data_relation_validation(self): class TestVersionedDataRelationCustomField(TestNormalVersioning): def setUp(self): - super(TestVersionedDataRelationCustomField, self).setUp() + super().setUp() # enable versioning in the invoice data_relation definition with custom # relation field @@ -1202,7 +1204,7 @@ def test_referential_integrity(self): class TestVersionedDataRelationUnversionedField(TestNormalVersioning): def setUp(self): - super(TestVersionedDataRelationUnversionedField, self).setUp() + super().setUp() # enable versioning in the invoice data_relation definition with custom # unversioned relation field @@ -1247,7 +1249,7 @@ def test_referential_integrity(self): class TestPartialVersioning(TestNormalVersioning): def setUp(self): - super(TestPartialVersioning, self).setUp() + super().setUp() self.enableVersioning(partial=True) self.insertTestData() @@ -1294,7 +1296,7 @@ def test_version_control_the_unkown(self): class TestLateVersioning(TestVersioningBase): def setUp(self): - super(TestLateVersioning, self).setUp() + super().setUp() # enable versioning in the invoice data_relation definition self.enableDataVersionRelation(embeddable=True) @@ -1492,7 +1494,7 @@ def test_embedded(self): class TestVersioningWithCustomIdField(TestNormalVersioning): def setUp(self): - super(TestVersioningWithCustomIdField, self).setUp() + super().setUp() self.domain[self.known_resource]["schema"][self.id_field] = {"type": "string"} self.enableVersioning() self.insertTestData() diff --git a/eve/utils.py b/eve/utils.py index d5fccdad2..384a74dc1 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -10,24 +10,24 @@ :license: BSD, see LICENSE for more details. """ +import hashlib import sys +from copy import deepcopy +from datetime import datetime, timedelta from importlib import import_module +import werkzeug.exceptions from bson import UuidRepresentation +from bson.json_util import dumps +from flask import current_app as app +from flask import request +from werkzeug.datastructures import MultiDict import eve -import hashlib -import werkzeug.exceptions -from copy import deepcopy -from flask import request -from flask import current_app as app -from datetime import datetime, timedelta -from bson.json_util import dumps from eve import RFC1123_DATE_FORMAT -from werkzeug.datastructures import MultiDict -class Config(object): +class Config(): """Helper class used through the code to access configuration settings. If the main flaskapp object is not instantiated yet, returns the default setting in the eve __init__.py module, otherwise returns the flaskapp @@ -39,7 +39,7 @@ def __getattr__(self, name): # will return 'working outside of application context' if the # current_app is not available yet return app.config.get(name) - except: + except Exception: # fallback to the module-level default value return getattr(eve, name) @@ -49,7 +49,7 @@ def __getattr__(self, name): config = Config() -class ParsedRequest(object): +class ParsedRequest(): """This class, by means of its attributes, describes a client request. .. versionchanged:: 9,5 @@ -172,8 +172,7 @@ def etag_parse(challenge): # remove double quotes from challenge etag format to allow direct # string comparison with stored values return etag.replace('"', "") - else: - return None + return None if headers: r.if_modified_since = weak_date(headers.get("If-Modified-Since")) @@ -476,7 +475,7 @@ def dict_sub_schema(base): # sized list sub = dict_sub_schema(base_schema["schema"]) return [sub] if sub is not None else [] - elif "items" in base_schema: + if "items" in base_schema: # Try to get dict sub-schema(s) for # fixed-size list items = base_schema["items"] @@ -505,10 +504,9 @@ def recursive_validate_filter(key, value, schema): return True return False - else: - field_schema = schema.get(key) - v = app.validator({key: field_schema}) - return v.validate({key: value}) + field_schema = schema.get(key) + v = app.validator({key: field_schema}) + return v.validate({key: value}) res_schema = config.DOMAIN[resource]["schema"] if not recursive_validate_filter(key, value, res_schema): diff --git a/eve/validation.py b/eve/validation.py index ea62b24ea..30bd8d5b2 100644 --- a/eve/validation.py +++ b/eve/validation.py @@ -13,6 +13,7 @@ """ import copy + import cerberus import cerberus.errors from cerberus import DocumentError, SchemaError # noqa @@ -26,7 +27,7 @@ def __init__(self, *args, **kwargs): kwargs["error_handler"] = SingleErrorAsStringErrorHandler self.is_update_operation = False - super(Validator, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate_update( self, document, document_id, persisted_document=None, normalize_document=True @@ -42,7 +43,7 @@ def validate_update( self.is_update_operation = True self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate( + return super().validate( document, update=True, normalize=normalize_document ) @@ -62,7 +63,7 @@ def validate_replace(self, document, document_id, persisted_document=None): """ self.document_id = document_id self.persisted_document = persisted_document - return super(Validator, self).validate(document) + return super().validate(document) def _normalize_default(self, mapping, schema, field): """{'nullable': True}""" @@ -86,7 +87,7 @@ def _normalize_default(self, mapping, schema, field): # - A PATCH to an existing document where the field is not set # - A PUT to a document where the field maybe is set - super(Validator, self)._normalize_default(mapping, schema, field) + super()._normalize_default(mapping, schema, field) def _normalize_default_setter(self, mapping, schema, field): """{'oneof': [ @@ -94,7 +95,7 @@ def _normalize_default_setter(self, mapping, schema, field): {'type': 'string'} ]}""" if not self.persisted_document or field not in self.persisted_document: - super(Validator, self)._normalize_default_setter(mapping, schema, field) + super()._normalize_default_setter(mapping, schema, field) def _validate_dependencies(self, dependencies, field, value): """{'type': ['dict', 'hashable', 'list']}""" @@ -107,7 +108,7 @@ def _validate_dependencies(self, dependencies, field, value): validator.validate(dcopy, update=self.update) self._error(validator._errors) else: - super(Validator, self)._validate_dependencies(dependencies, field, value) + super()._validate_dependencies(dependencies, field, value) def _filter_persisted_fields_not_in_document(self, fields): def persisted_but_not_in_document(field): @@ -125,7 +126,7 @@ def _validate_readonly(self, read_only, field, value): self.persisted_document.get(field) if self.persisted_document else None ) if value != persisted_value: - super(Validator, self)._validate_readonly(read_only, field, value) + super()._validate_readonly(read_only, field, value) @property def resource(self): @@ -163,7 +164,7 @@ class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler): @property def pretty_tree(self): - pretty = super(SingleErrorAsStringErrorHandler, self).pretty_tree + pretty = super().pretty_tree self._unpack_single_element_lists(pretty) return pretty diff --git a/eve/versioning.py b/eve/versioning.py index 08cb0c712..4506d2e76 100644 --- a/eve/versioning.py +++ b/eve/versioning.py @@ -1,7 +1,9 @@ -from flask import current_app as app, abort -from eve.utils import config, debug_error_message, ParsedRequest +from flask import abort +from flask import current_app as app from werkzeug.exceptions import BadRequestKeyError +from eve.utils import ParsedRequest, config, debug_error_message + def versioned_id_field(resource_settings): """Shorthand to add two commonly added versioning parameters. diff --git a/examples/notifications.py b/examples/notifications.py index 00b92e426..3c2e5204b 100644 --- a/examples/notifications.py +++ b/examples/notifications.py @@ -18,9 +18,10 @@ Consider it public domain. """ from flask import request -from eve import Eve from notifications_settings import SETTINGS +from eve import Eve + app = Eve(auth=None, settings=SETTINGS) diff --git a/examples/security/bcrypt.py b/examples/security/bcrypt.py index 46e60706c..a146e9519 100644 --- a/examples/security/bcrypt.py +++ b/examples/security/bcrypt.py @@ -21,9 +21,10 @@ """ import bcrypt +from settings_security import SETTINGS + from eve import Eve from eve.auth import BasicAuth -from settings_security import SETTINGS class BCryptAuth(BasicAuth): diff --git a/examples/security/hmac.py b/examples/security/hmac.py index 976106cb3..dc06bc1bb 100644 --- a/examples/security/hmac.py +++ b/examples/security/hmac.py @@ -46,13 +46,13 @@ Consider it public domain. """ import hmac - -from eve import Eve -from eve.auth import HMACAuth from hashlib import sha1 from settings_security import SETTINGS +from eve import Eve +from eve.auth import HMACAuth + class HMACAuth(HMACAuth): def check_auth( diff --git a/examples/security/roles.py b/examples/security/roles.py index 85ce2a048..83727714a 100644 --- a/examples/security/roles.py +++ b/examples/security/roles.py @@ -23,11 +23,11 @@ Consider it public domain. """ -from eve import Eve -from eve.auth import BasicAuth +from settings_security import SETTINGS from werkzeug.security import check_password_hash -from settings_security import SETTINGS +from eve import Eve +from eve.auth import BasicAuth class RolesAuth(BasicAuth): diff --git a/examples/security/sha1-hmac.py b/examples/security/sha1-hmac.py index c3ec77d12..b72f232e8 100644 --- a/examples/security/sha1-hmac.py +++ b/examples/security/sha1-hmac.py @@ -21,11 +21,11 @@ Consider it public domain. """ -from eve import Eve -from eve.auth import BasicAuth +from settings_security import SETTINGS from werkzeug.security import check_password_hash -from settings_security import SETTINGS +from eve import Eve +from eve.auth import BasicAuth class Sha1Auth(BasicAuth): diff --git a/examples/security/token.py b/examples/security/token.py index 416d71282..9c03291c6 100644 --- a/examples/security/token.py +++ b/examples/security/token.py @@ -20,11 +20,11 @@ Consider it public domain. """ +from settings_security import SETTINGS + from eve import Eve from eve.auth import TokenAuth -from settings_security import SETTINGS - class TokenAuth(TokenAuth): def check_auth(self, token, allowed_roles, resource, method): diff --git a/setup.py b/setup.py index b0389cf5c..09eeed612 100755 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ #!/usr/bin/env python import io import re - -from setuptools import setup, find_packages from collections import OrderedDict +from setuptools import find_packages, setup + DESCRIPTION = "Python REST API for Humans." with open("README.rst") as f: LONG_DESCRIPTION = f.read() From 71689d4c8ca1419c1686c9d8d4ce33eb442c914a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 8 Nov 2022 16:00:15 +0100 Subject: [PATCH 749/821] fix: comparison of incompatible types. Addresses #1492 --- CHANGES.rst | 4 +++- eve/methods/common.py | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 869b49af6..1213d1685 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Fix: comparison of incompatible types (`#1492`_) + +.. _`#1492`: https://github.com/pyeve/eve/issues/1492 Version v2.0.3 -------------- diff --git a/eve/methods/common.py b/eve/methods/common.py index 220fc42da..bca5118a1 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -754,11 +754,7 @@ def resolve_data_relation_links(document, resource): if "data_relation" not in field_def: continue - if ( - field in document - and document[field] is not None - and document[field] is not [] - ): + if field in document and document[field] is not None and document[field] != []: related_links = [] # Make the code DRY for list of linked relation and single linked relation From c1e1a819bdba83f3103b9e8295dc8b6a0c651512 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Nov 2022 10:13:55 +0100 Subject: [PATCH 750/821] fix the imports order in mongo data layer --- eve/io/mongo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/io/mongo/__init__.py b/eve/io/mongo/__init__.py index 08e28fef0..f2616c72b 100644 --- a/eve/io/mongo/__init__.py +++ b/eve/io/mongo/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -from eve.io.mongo.media import GridFSMediaStorage # flake8: noqa from eve.io.mongo.mongo import Mongo, MongoJSONEncoder, ensure_mongo_indexes +from eve.io.mongo.media import GridFSMediaStorage from eve.io.mongo.validation import Validator From 4d176fbc3f1d0c7f4ebe520a473db016727c0c7d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Nov 2022 10:19:47 +0100 Subject: [PATCH 751/821] Mark Mayo --- AUTHORS | 3 ++- CHANGES.rst | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 22af1eed6..624332028 100644 --- a/AUTHORS +++ b/AUTHORS @@ -122,6 +122,7 @@ Patches and Contributions - Marcus Cobden - Marica Odagaki - Mario Kralj +- Mark Mayo - Marsch Huynh - Martin Fous - Massimo Scamarcia @@ -212,4 +213,4 @@ Patches and Contributions - quentinpraz - smeng9 - tgm -- xgdgsc \ No newline at end of file +- xgdgsc diff --git a/CHANGES.rst b/CHANGES.rst index 869b49af6..1c70e615a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- hic sunt leones. +- Python 3 updates, and some refactoring (`#1493`_) + +.. _`#1493`: https://github.com/pyeve/eve/pull/1493 Version v2.0.3 -------------- From 9a79efd45c7ee8fa89ce85b26aada21e251d2616 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Nov 2022 10:52:29 +0100 Subject: [PATCH 752/821] bump version to 2.0.4 --- CHANGES.rst | 12 +++++++++++- eve/__init__.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 282084b55..8edd96226 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,17 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- Fix: comparison of incompatible types (`#1492`_) +- *hic sunt leones* + +Version v2.0.4 +-------------- + +Released on Nov 10, 2022. + +Fixed +~~~~~ + +- Comparison of incompatible types (`#1492`_) - Python 3 updates, and some refactoring (`#1493`_) .. _`#1492`: https://github.com/pyeve/eve/issues/1492 diff --git a/eve/__init__.py b/eve/__init__.py index b1e17324b..9f1d0a970 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.4-dev0" +__version__ = "2.0.4" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From e3902e7726038a61149e4344676cd1263bc29d34 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 10 Nov 2022 10:54:26 +0100 Subject: [PATCH 753/821] bump version to 2.0.5-dev --- eve/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve/__init__.py b/eve/__init__.py index 9f1d0a970..2bef9ee1c 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.4" +__version__ = "2.0.5-dev" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 88fd8a111d86109e7390fdc44a38904e604f7f88 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 10 Mar 2023 18:14:21 +0100 Subject: [PATCH 754/821] use mongosh --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64fde5662..a3dbef486 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,6 @@ jobs: python -m pip install --upgrade virtualenv tox tox-gh-actions - name: Start mongo ${{ matrix.mongodb-version }} run: | - mongo eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' + mongosh eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' - name: Run tox targets for ${{ matrix.python }} run: tox -e ${{ matrix.tox }} From 12114e5863396d55af34e7615ce72677754ef155 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 10 Mar 2023 18:24:24 +0100 Subject: [PATCH 755/821] CI: pin ubuntu on 20.04. Closes #1499. Pinning ubuntu to v20.04 so CI runs can keep going. Ideally, we should fix the GitHub Action to use the new MongoDB Shell, mongosh. --- .github/workflows/ci.yml | 12 ++++++------ CHANGES.rst | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3dbef486..524bb4a9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,11 +10,11 @@ jobs: strategy: matrix: include: - - { name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310, mongodb: '4.4', redis: '6' } - - { name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39, mongodb: '4.4', redis: '6' } - - { name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38, mongodb: '4.4', redis: '6' } - - { name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37, mongodb: '4.4', redis-version: '6' } - - { name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37, mongodb: '4.4', redis: '6' } + - { name: '3.10', python: '3.10', os: ubuntu-20.04, tox: py310, mongodb: '4.4', redis: '6' } + - { name: '3.9', python: '3.9', os: ubuntu-20.04, tox: py39, mongodb: '4.4', redis: '6' } + - { name: '3.8', python: '3.8', os: ubuntu-20.04, tox: py38, mongodb: '4.4', redis: '6' } + - { name: '3.7', python: '3.7', os: ubuntu-20.04, tox: py37, mongodb: '4.4', redis-version: '6' } + - { name: 'PyPy', python: 'pypy-3.7', os: ubuntu-20.04, tox: pypy37, mongodb: '4.4', redis: '6' } steps: - uses: actions/checkout@v2 @@ -36,6 +36,6 @@ jobs: python -m pip install --upgrade virtualenv tox tox-gh-actions - name: Start mongo ${{ matrix.mongodb-version }} run: | - mongosh eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' + mongo eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' - name: Run tox targets for ${{ matrix.python }} run: tox -e ${{ matrix.tox }} diff --git a/CHANGES.rst b/CHANGES.rst index 8edd96226..a960ef22b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt leones* +- fix: CI test runs fail with `mongo command not found` on Ubuntu 22.04 (`#1499`_) + +.. _`#1499`: https://github.com/pyeve/eve/issues/1499 Version v2.0.4 -------------- From c1ad0d53c0b82e4a418e3d043bf7ab834e1e6ec5 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 10 Mar 2023 18:39:42 +0100 Subject: [PATCH 756/821] changelog typo --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a960ef22b..f7e54637f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- fix: CI test runs fail with `mongo command not found` on Ubuntu 22.04 (`#1499`_) +- fix: CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) .. _`#1499`: https://github.com/pyeve/eve/issues/1499 From 377e48cbc8e83770137a79943d751c9aadd16534 Mon Sep 17 00:00:00 2001 From: Pieter De Clercq Date: Thu, 9 Mar 2023 19:04:02 +0100 Subject: [PATCH 757/821] Add test --- eve/tests/methods/get.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/eve/tests/methods/get.py b/eve/tests/methods/get.py index 0fdac0f39..87eda7401 100644 --- a/eve/tests/methods/get.py +++ b/eve/tests/methods/get.py @@ -45,6 +45,21 @@ def test_get_max_results(self): resource = response["_items"] self.assertEqual(len(resource), self.app.config["PAGINATION_LIMIT"]) + def test_get_max_results_overridden(self): + # Generate 50 contacts. + self.random_contacts(num=50) + + # Set the max pagination limit to 7. + self.app.config["DOMAIN"][self.known_resource]["pagination_limit"] = 7 + + # Attempt to get all 50 contacts in one request. + response, status = self.get(self.known_resource, "?max_results=50") + self.assert200(status) + + # Validate that the response only contains 10 contacts. + resource = response["_items"] + self.assertEqual(len(resource), 7) + def test_get_custom_max_results(self): self.app.config["QUERY_MAX_RESULTS"] = "size" maxr = 10 From 0f5633450edcb22dbcee98b3991052c55a320716 Mon Sep 17 00:00:00 2001 From: Pieter De Clercq Date: Thu, 9 Mar 2023 19:18:48 +0100 Subject: [PATCH 758/821] Implement feature --- eve/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eve/utils.py b/eve/utils.py index 384a74dc1..b0a0e249d 100644 --- a/eve/utils.py +++ b/eve/utils.py @@ -160,8 +160,10 @@ def parse_request(resource): # TODO should probably return a 400 if 'max_results' < 1 or # non-numeric - if r.max_results > config.PAGINATION_LIMIT: - r.max_results = config.PAGINATION_LIMIT + # Fetch the custom pagination limit from the schema, default to the global one. + pagination_limit = settings.get("pagination_limit") or config.PAGINATION_LIMIT + if r.max_results > pagination_limit: + r.max_results = pagination_limit def etag_parse(challenge): if challenge in headers: From fb5cf7abfd2727edbc625af4e52721a75b9c7572 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 Mar 2023 08:23:37 +0100 Subject: [PATCH 759/821] changelog for #1498 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f7e54637f..1f283c0b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- new: Ability to customize the pagination limit on a per-resource basis (`#1498`_) - fix: CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) .. _`#1499`: https://github.com/pyeve/eve/issues/1499 +.. _`#1498`: https://github.com/pyeve/eve/issues/1498 Version v2.0.4 -------------- From b42f03ad343c348b8a1ae8f49151cb73cbd9b4fe Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 Mar 2023 08:24:29 +0100 Subject: [PATCH 760/821] Pieter De Clercq --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 624332028..d12e9fcf1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ Patches and Contributions - Peter Darrow - Petr Jašek - Phone Myint Kyaw +- Pieter De Clercq - Prajjwal Nijhara - Prayag Verma - Qiang Zhang From 1ae27fa7ea834eeec2acbd14fd992b8609bad1bd Mon Sep 17 00:00:00 2001 From: motion Date: Mon, 6 Mar 2023 21:51:23 +0800 Subject: [PATCH 761/821] fix flask 2.2 tests --- eve/tests/__init__.py | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/eve/tests/__init__.py b/eve/tests/__init__.py index 8eb50975b..03e2e034c 100644 --- a/eve/tests/__init__.py +++ b/eve/tests/__init__.py @@ -61,6 +61,15 @@ def close_pymongo_connection(app): del app.media +def setup_add_url_rule(app, original_add_url_rule): + def wrapped_add_url_rule(*args, **kwargs): + original_got_first_request = app._got_first_request + app._got_first_request = False + original_add_url_rule(*args, **kwargs) + app._got_first_request = original_got_first_request + return wrapped_add_url_rule + + class TestMinimal(unittest.TestCase): """Start the building of the tests for an application based on Eve by subclassing this class and provide proper settings @@ -482,6 +491,8 @@ def setUp(self, url_converters=None): self.test_patch = "test_patch" self.test_patch_url = "/%s" % self.domain[self.test_patch]["url"] + self.app.add_url_rule = setup_add_url_rule(self.app, self.app.add_url_rule) + def response_item(self, response, i=0): if self.app.config["HATEOAS"]: return response["_items"][i] diff --git a/setup.py b/setup.py index 09eeed612..b262f7c0e 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ INSTALL_REQUIRES = [ "cerberus>=1.1,<2.0", "events>=0.3,<0.4", - "flask<2.2", + "flask", "pymongo", "simplejson>=3.3.0,<4.0", ] From 415fbf8f4d88de277d284eb06e159dc5174b41d7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Sat, 11 Mar 2023 08:28:23 +0100 Subject: [PATCH 762/821] changelog for #1497 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1f283c0b9..6b273cca8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,10 +7,12 @@ In Development --------------- - new: Ability to customize the pagination limit on a per-resource basis (`#1498`_) +- fix: Tets fail with Flask 2.2 (`#1497`_) - fix: CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) .. _`#1499`: https://github.com/pyeve/eve/issues/1499 .. _`#1498`: https://github.com/pyeve/eve/issues/1498 +.. _`#1497`: https://github.com/pyeve/eve/issues/1497 Version v2.0.4 -------------- From 38d99f214244f77e72435dfa1dc49b15d793133a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Mar 2023 09:06:02 +0100 Subject: [PATCH 763/821] docs for #1498 --- docs/config.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 3baeb266a..02039b39d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -158,14 +158,14 @@ uppercase. overridden by resource settings. Defaults to ``True``. -``PAGINATION_LIMIT`` Maximum value allowed for QUERY_MAX_RESULTS +``PAGINATION_LIMIT`` Maximum value allowed for ``QUERY_MAX_RESULTS`` query parameter. Values exceeding the limit will be silently replaced with this value. You want to aim for a reasonable compromise between performance and transfer size. Defaults to 50. -``PAGINATION_DEFAULT`` Default value for QUERY_MAX_RESULTS. +``PAGINATION_DEFAULT`` Default value for ``QUERY_MAX_RESULTS``. Defaults to 25. ``OPTIMIZE_PAGINATION_FOR_SPEED`` Set this to ``True`` to improve pagination @@ -868,6 +868,13 @@ always lowercase. ``pagination`` ``True`` if pagination is enabled, ``False`` otherwise. Locally overrides ``PAGINATION``. +``pagination_limit`` Maximum value allowed for ``QUERY_MAX_RESULTS`` + query parameter. Values exceeding the + limit will be silently replaced with this + value. You want to aim for a reasonable + compromise between performance and transfer + size. Defaults to 50. + ``resource_methods`` A list of HTTP methods supported at resource endpoint. Allowed values: ``GET``, ``POST``, ``DELETE``. Locally overrides From 9515cdaf269a9f2f07164829f1be1829348376a4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 14 Mar 2023 09:12:04 +0100 Subject: [PATCH 764/821] bump version to 2.1.0 --- CHANGES.rst | 18 ++++++++++++++++-- eve/__init__.py | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6b273cca8..332a68893 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,22 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- new: Ability to customize the pagination limit on a per-resource basis (`#1498`_) -- fix: Tets fail with Flask 2.2 (`#1497`_) +- *hic sunt leones* + +Version v2.1.0 +-------------- + +Released on Mar 14, 2023. + +New +~~~ + +- Ability to customize the pagination limit on a per-resource basis (`#1498`_) + +Fixed +~~~~~ + +- fix: Flask 2.2+ support (`#1497`_) - fix: CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) .. _`#1499`: https://github.com/pyeve/eve/issues/1499 diff --git a/eve/__init__.py b/eve/__init__.py index 2bef9ee1c..daf6ea305 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.0.5-dev" +__version__ = "2.1.0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From d78e2f90ec4f3a1a317e43972ec32c03065a97ed Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 22 Mar 2023 14:32:23 +0100 Subject: [PATCH 765/821] add .venv/ to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 77fc9f3e6..f681a98af 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ _build .pytest_cache pip-wheel-metadata/ !/.eggs/ +.venv/ From 4af65a9406d627890b896fd8f249ddca4024520e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Jun 2023 08:38:01 +0200 Subject: [PATCH 766/821] update .readthedocs.yml to v2 --- .readthedocs.yml | 18 +++++++++++++++--- docs/requirements.txt | 7 +++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml index 788bab33b..510cd6c45 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,4 +1,16 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + python: - pip_install: true - extra_requirements: - - docs + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..f90e0a454 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +# requirements to build documentation +Sphinx<2.0 +sphinxcontrib-issuetracker +alabaster +doc8 +eve +jinja2<3.1.0 From 4aa85d153b9e7bcb1d7a2380a9a7058b976831aa Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Jun 2023 09:17:09 +0200 Subject: [PATCH 767/821] Python 3.11 added to the CI matrix --- .github/workflows/ci.yml | 1 + CHANGES.rst | 3 ++- setup.py | 1 + tox.ini | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 524bb4a9b..51c033e8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: strategy: matrix: include: + - { name: '3.11', python: '3.11', os: ubuntu-20.04, tox: py311, mongodb: '4.4', redis: '6' } - { name: '3.10', python: '3.10', os: ubuntu-20.04, tox: py310, mongodb: '4.4', redis: '6' } - { name: '3.9', python: '3.9', os: ubuntu-20.04, tox: py39, mongodb: '4.4', redis: '6' } - { name: '3.8', python: '3.8', os: ubuntu-20.04, tox: py38, mongodb: '4.4', redis: '6' } diff --git a/CHANGES.rst b/CHANGES.rst index 332a68893..7ae37e631 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,8 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt leones* +- Python 3.11 added to the CI matrix. +- .readthedocs.yml upgraded to V2. Version v2.1.0 -------------- diff --git a/setup.py b/setup.py index b262f7c0e..382be5119 100755 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", diff --git a/tox.ini b/tox.ini index 1f99c669f..c69ad3831 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py3{10,9,8,7},pypy3{8,7},linting +envlist=py3{11,10,9,8,7},pypy3{8,7},linting [testenv] extras=tests From b80642f1ac5ef8e2f94264b42ac233e80f593624 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 27 Jun 2023 09:46:42 +0200 Subject: [PATCH 768/821] changelog cleanup --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7ae37e631..c056427c8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,8 +22,8 @@ New Fixed ~~~~~ -- fix: Flask 2.2+ support (`#1497`_) -- fix: CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) +- Flask 2.2+ support (`#1497`_) +- CI test runs fail with ``mongo: command not found`` on Ubuntu 22.04 (`#1499`_) .. _`#1499`: https://github.com/pyeve/eve/issues/1499 .. _`#1498`: https://github.com/pyeve/eve/issues/1498 From 1711d8f33f694b6654ddde0bde45d5846dfcded5 Mon Sep 17 00:00:00 2001 From: Guillaume Le Pape Date: Fri, 7 Jul 2023 19:50:49 +0200 Subject: [PATCH 769/821] Moving tests folder to root Mainly to avoid including it in python wheel --- .pre-commit-config.yaml | 2 +- setup.py | 4 ++-- {eve/tests => tests}/__init__.py | 14 ++++++++---- {eve/tests => tests}/auth.py | 5 +++-- {eve/tests => tests}/config.py | 6 ++--- {eve/tests => tests}/endpoints.py | 14 ++++++------ {eve/tests => tests}/methods/__init__.py | 0 {eve/tests => tests}/methods/common.py | 11 +++++---- {eve/tests => tests}/methods/delete.py | 6 ++--- {eve/tests => tests}/methods/get.py | 15 +++++-------- {eve/tests => tests}/methods/patch.py | 6 ++--- .../methods/patch_atomic_concurrency.py | 3 ++- {eve/tests => tests}/methods/post.py | 6 ++--- {eve/tests => tests}/methods/put.py | 6 ++--- {eve/tests => tests}/methods/ratelimit.py | 2 +- {eve/tests => tests}/renders.py | 5 +++-- {eve/tests => tests}/response.py | 3 ++- {eve/tests => tests}/suite_generator.py | 0 {eve/tests => tests}/test.db | Bin {eve/tests => tests}/test_io/__init__.py | 0 {eve/tests => tests}/test_io/flask_pymongo.py | 11 ++++++--- {eve/tests => tests}/test_io/media.py | 2 +- {eve/tests => tests}/test_io/mongo.py | 4 ++-- {eve/tests => tests}/test_io/multi_mongo.py | 13 +++++++---- {eve/tests => tests}/test_logging.py | 2 +- {eve/tests => tests}/test_prefix.py | 0 {eve/tests => tests}/test_prefix_version.py | 0 {eve/tests => tests}/test_settings.py | 0 {eve/tests => tests}/test_settings_env.py | 0 {eve/tests => tests}/test_version.py | 0 {eve/tests => tests}/utils.py | 21 +++++++++++++----- {eve/tests => tests}/versioning.py | 7 +++--- tox.ini | 2 +- 33 files changed, 99 insertions(+), 71 deletions(-) rename {eve/tests => tests}/__init__.py (99%) rename {eve/tests => tests}/auth.py (99%) rename {eve/tests => tests}/config.py (99%) rename {eve/tests => tests}/endpoints.py (97%) rename {eve/tests => tests}/methods/__init__.py (100%) rename {eve/tests => tests}/methods/common.py (98%) rename {eve/tests => tests}/methods/delete.py (99%) rename {eve/tests => tests}/methods/get.py (99%) rename {eve/tests => tests}/methods/patch.py (99%) rename {eve/tests => tests}/methods/patch_atomic_concurrency.py (98%) rename {eve/tests => tests}/methods/post.py (99%) rename {eve/tests => tests}/methods/put.py (99%) rename {eve/tests => tests}/methods/ratelimit.py (98%) rename {eve/tests => tests}/renders.py (99%) rename {eve/tests => tests}/response.py (99%) rename {eve/tests => tests}/suite_generator.py (100%) rename {eve/tests => tests}/test.db (100%) rename {eve/tests => tests}/test_io/__init__.py (100%) rename {eve/tests => tests}/test_io/flask_pymongo.py (93%) rename {eve/tests => tests}/test_io/media.py (99%) rename {eve/tests => tests}/test_io/mongo.py (99%) rename {eve/tests => tests}/test_io/multi_mongo.py (97%) rename {eve/tests => tests}/test_logging.py (95%) rename {eve/tests => tests}/test_prefix.py (100%) rename {eve/tests => tests}/test_prefix_version.py (100%) rename {eve/tests => tests}/test_settings.py (100%) rename {eve/tests => tests}/test_settings_env.py (100%) rename {eve/tests => tests}/test_version.py (100%) rename {eve/tests => tests}/utils.py (98%) rename {eve/tests => tests}/versioning.py (99%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ca758be1..3efa5ba63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 23.3.0 hooks: - id: black language_version: python3.9 diff --git a/setup.py b/setup.py index 382be5119..319de7056 100755 --- a/setup.py +++ b/setup.py @@ -44,8 +44,8 @@ ), license="BSD", platforms=["any"], - packages=find_packages(), - test_suite="eve.tests", + packages=find_packages(exclude=["tests*"]), + test_suite="tests", install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, python_requires=">=3.7", diff --git a/eve/tests/__init__.py b/tests/__init__.py similarity index 99% rename from eve/tests/__init__.py rename to tests/__init__.py index 03e2e034c..8c23e3575 100644 --- a/eve/tests/__init__.py +++ b/tests/__init__.py @@ -12,9 +12,14 @@ import eve from eve import ETAG, ISSUES -from eve.tests.test_settings import (DOMAIN, MONGO_DBNAME, MONGO_HOST, - MONGO_PASSWORD, MONGO_PORT, - MONGO_USERNAME) +from .test_settings import ( + DOMAIN, + MONGO_DBNAME, + MONGO_HOST, + MONGO_PASSWORD, + MONGO_PORT, + MONGO_USERNAME, +) from eve.utils import date_to_str try: @@ -23,7 +28,7 @@ from urllib.parse import parse_qs, urlparse -class ValueStack(): +class ValueStack: """ Descriptor to store multiple assignments in an attribute. @@ -67,6 +72,7 @@ def wrapped_add_url_rule(*args, **kwargs): app._got_first_request = False original_add_url_rule(*args, **kwargs) app._got_first_request = original_got_first_request + return wrapped_add_url_rule diff --git a/eve/tests/auth.py b/tests/auth.py similarity index 99% rename from eve/tests/auth.py rename to tests/auth.py index d2846e8e6..a86c7887d 100644 --- a/eve/tests/auth.py +++ b/tests/auth.py @@ -7,8 +7,9 @@ import eve from eve import Eve from eve.auth import BasicAuth, HMACAuth, TokenAuth -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME + +from . import TestBase +from .test_settings import MONGO_DBNAME class ValidBasicAuth(BasicAuth): diff --git a/eve/tests/config.py b/tests/config.py similarity index 99% rename from eve/tests/config.py rename to tests/config.py index e29d66bd2..612822065 100644 --- a/eve/tests/config.py +++ b/tests/config.py @@ -7,8 +7,9 @@ from eve.flaskapp import Eve, RegexConverter from eve.io.base import DataLayer from eve.io.mongo import Mongo, Validator -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_HOST, MONGO_PORT + +from . import TestBase +from .test_settings import MONGO_HOST, MONGO_PORT class TestConfig(TestBase): @@ -423,7 +424,6 @@ def test_auth_field_as_custom_idfield(self): ) def test_oplog_config(self): - # if OPLOG_ENDPOINT is enabled the endoint is included with the domain self.app.config["OPLOG_ENDPOINT"] = "oplog" self.app._init_oplog() diff --git a/eve/tests/endpoints.py b/tests/endpoints.py similarity index 97% rename from eve/tests/endpoints.py rename to tests/endpoints.py index a4ccef7f9..183c1fc62 100644 --- a/eve/tests/endpoints.py +++ b/tests/endpoints.py @@ -10,11 +10,11 @@ from eve import Eve from eve.io.base import BaseJSONEncoder from eve.io.mongo import Validator -from eve.tests import TestBase, TestMinimal -from eve.tests.test_settings import (MONGO_DBNAME, MONGO_PASSWORD, - MONGO_USERNAME) from eve.utils import config +from . import TestBase, TestMinimal +from .test_settings import MONGO_DBNAME, MONGO_PASSWORD, MONGO_USERNAME + class UUIDEncoder(BaseJSONEncoder): """Propretary JSONEconder subclass used by the json render function. @@ -80,9 +80,7 @@ def setUp(self): self.url = "/uuids/%s" % self.uuid_valid self.headers = [("Content-Type", "application/json")] - super().setUp( - settings_file=settings, url_converters=url_converters - ) + super().setUp(settings_file=settings, url_converters=url_converters) self.app.validator = UUIDValidator self.app.data.json_encoder_class = UUIDEncoder @@ -355,7 +353,9 @@ def test_schema_endpoint(self): def test_schema_endpoint_does_not_attempt_callable_serialization(self): self.domain[self.known_resource]["schema"]["lambda"] = { "type": "boolean", - "coerce": lambda v: v if isinstance(v, bool) else v.lower() in ["true", "1"], + "coerce": lambda v: v + if isinstance(v, bool) + else v.lower() in ["true", "1"], } known_schema_path = "/schema/%s" % self.known_resource self.app.config["SCHEMA_ENDPOINT"] = "schema" diff --git a/eve/tests/methods/__init__.py b/tests/methods/__init__.py similarity index 100% rename from eve/tests/methods/__init__.py rename to tests/methods/__init__.py diff --git a/eve/tests/methods/common.py b/tests/methods/common.py similarity index 98% rename from eve/tests/methods/common.py rename to tests/methods/common.py index 5ad11bb53..6831765d7 100644 --- a/eve/tests/methods/common.py +++ b/tests/methods/common.py @@ -7,13 +7,12 @@ from bson import ObjectId, decimal128 from bson.dbref import DBRef -from eve.methods.common import (normalize_dotted_fields, serialize, - sort_per_resource) -from eve.tests import TestBase -from eve.tests.auth import ValidBasicAuth, ValidHMACAuth, ValidTokenAuth -from eve.tests.suite_generator import EmbeddedDoc -from eve.tests.test_settings import MONGO_DBNAME +from eve.methods.common import normalize_dotted_fields, serialize, sort_per_resource from eve.utils import config +from tests import TestBase +from tests.auth import ValidBasicAuth, ValidHMACAuth, ValidTokenAuth +from tests.suite_generator import EmbeddedDoc +from tests.test_settings import MONGO_DBNAME class TestSerializer(TestBase): diff --git a/eve/tests/methods/delete.py b/tests/methods/delete.py similarity index 99% rename from eve/tests/methods/delete.py rename to tests/methods/delete.py index 0fd84872d..b971fbbab 100644 --- a/eve/tests/methods/delete.py +++ b/tests/methods/delete.py @@ -5,10 +5,10 @@ from eve import ETAG from eve.methods.delete import deleteitem_internal -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent from eve.utils import ParsedRequest +from tests import TestBase +from tests.test_settings import MONGO_DBNAME +from tests.utils import DummyEvent class TestDelete(TestBase): diff --git a/eve/tests/methods/get.py b/tests/methods/get.py similarity index 99% rename from eve/tests/methods/get.py rename to tests/methods/get.py index 87eda7401..480528b99 100644 --- a/eve/tests/methods/get.py +++ b/tests/methods/get.py @@ -10,10 +10,10 @@ from werkzeug.datastructures import ImmutableMultiDict, MultiDict from eve.methods.get import get_internal, getitem_internal -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent from eve.utils import date_to_rfc1123, str_to_date +from tests import TestBase +from tests.test_settings import MONGO_DBNAME +from tests.utils import DummyEvent class TestGet(TestBase): @@ -48,14 +48,14 @@ def test_get_max_results(self): def test_get_max_results_overridden(self): # Generate 50 contacts. self.random_contacts(num=50) - + # Set the max pagination limit to 7. self.app.config["DOMAIN"][self.known_resource]["pagination_limit"] = 7 - + # Attempt to get all 50 contacts in one request. response, status = self.get(self.known_resource, "?max_results=50") self.assert200(status) - + # Validate that the response only contains 10 contacts. resource = response["_items"] self.assertEqual(len(resource), 7) @@ -1463,7 +1463,6 @@ def test_get_subresource_with_custom_idfield(self): self.assertEqual(response["_items"][0]["parent_product"], parent_product_sku) def test_get_aggregation_endpoint(self): - _db = self.connection[MONGO_DBNAME] _db.aggregate_test.insert_many( [ @@ -1547,7 +1546,6 @@ def assertOutput(doc, count, id): self.assertEqual(len(docs), 1) def test_get_aggregation_parsing(self): - date = datetime.utcnow() _db = self.connection[MONGO_DBNAME] @@ -1633,7 +1631,6 @@ def test_get_aggregation_with_lists(self): self.assertEqual(len(docs), 1) def test_get_aggregation_pruning(self): - date = datetime.utcnow() _db = self.connection[MONGO_DBNAME] diff --git a/eve/tests/methods/patch.py b/tests/methods/patch.py similarity index 99% rename from eve/tests/methods/patch.py rename to tests/methods/patch.py index 8998d32df..b26a1530e 100644 --- a/eve/tests/methods/patch.py +++ b/tests/methods/patch.py @@ -4,9 +4,9 @@ from eve import ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK from eve.methods.patch import patch_internal -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent +from tests import TestBase +from tests.test_settings import MONGO_DBNAME +from tests.utils import DummyEvent class TestPatch(TestBase): diff --git a/eve/tests/methods/patch_atomic_concurrency.py b/tests/methods/patch_atomic_concurrency.py similarity index 98% rename from eve/tests/methods/patch_atomic_concurrency.py rename to tests/methods/patch_atomic_concurrency.py index 944560183..8e7857746 100644 --- a/eve/tests/methods/patch_atomic_concurrency.py +++ b/tests/methods/patch_atomic_concurrency.py @@ -3,9 +3,10 @@ import simplejson as json import eve.methods.common -from eve.tests import TestBase from eve.utils import config +from tests import TestBase + """ Atomic Concurrency Checks diff --git a/eve/tests/methods/post.py b/tests/methods/post.py similarity index 99% rename from eve/tests/methods/post.py rename to tests/methods/post.py index 0b4c6cf89..f45585023 100644 --- a/eve/tests/methods/post.py +++ b/tests/methods/post.py @@ -7,10 +7,10 @@ from eve import DATE_CREATED, ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK from eve.methods.post import post, post_internal -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent from eve.utils import str_type +from tests import TestBase +from tests.test_settings import MONGO_DBNAME +from tests.utils import DummyEvent class TestPost(TestBase): diff --git a/eve/tests/methods/put.py b/tests/methods/put.py similarity index 99% rename from eve/tests/methods/put.py rename to tests/methods/put.py index afb8d1b17..412243414 100644 --- a/eve/tests/methods/put.py +++ b/tests/methods/put.py @@ -4,9 +4,9 @@ from eve import ETAG, ISSUES, LAST_UPDATED, STATUS, STATUS_OK from eve.methods.put import put_internal -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent +from tests import TestBase +from tests.test_settings import MONGO_DBNAME +from tests.utils import DummyEvent class TestPut(TestBase): diff --git a/eve/tests/methods/ratelimit.py b/tests/methods/ratelimit.py similarity index 98% rename from eve/tests/methods/ratelimit.py rename to tests/methods/ratelimit.py index 85cc91057..1049c544c 100644 --- a/eve/tests/methods/ratelimit.py +++ b/tests/methods/ratelimit.py @@ -1,6 +1,6 @@ import time -from eve.tests import TestBase +from tests import TestBase class TestRateLimit(TestBase): diff --git a/eve/tests/renders.py b/tests/renders.py similarity index 99% rename from eve/tests/renders.py rename to tests/renders.py index 57879e468..bd3d46b93 100644 --- a/eve/tests/renders.py +++ b/tests/renders.py @@ -3,10 +3,11 @@ import simplejson as json from bson import ObjectId -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME from eve.utils import api_prefix +from . import TestBase +from .test_settings import MONGO_DBNAME + class TestRenders(TestBase): def test_default_render(self): diff --git a/eve/tests/response.py b/tests/response.py similarity index 99% rename from eve/tests/response.py rename to tests/response.py index 5e6a527ab..07c558675 100644 --- a/eve/tests/response.py +++ b/tests/response.py @@ -6,7 +6,8 @@ import simplejson as json import eve -from eve.tests import TestBase + +from . import TestBase class TestResponse(TestBase): diff --git a/eve/tests/suite_generator.py b/tests/suite_generator.py similarity index 100% rename from eve/tests/suite_generator.py rename to tests/suite_generator.py diff --git a/eve/tests/test.db b/tests/test.db similarity index 100% rename from eve/tests/test.db rename to tests/test.db diff --git a/eve/tests/test_io/__init__.py b/tests/test_io/__init__.py similarity index 100% rename from eve/tests/test_io/__init__.py rename to tests/test_io/__init__.py diff --git a/eve/tests/test_io/flask_pymongo.py b/tests/test_io/flask_pymongo.py similarity index 93% rename from eve/tests/test_io/flask_pymongo.py rename to tests/test_io/flask_pymongo.py index 710c8e78f..77101e63d 100644 --- a/eve/tests/test_io/flask_pymongo.py +++ b/tests/test_io/flask_pymongo.py @@ -3,9 +3,14 @@ from pymongo.errors import OperationFailure from eve.io.mongo.flask_pymongo import PyMongo -from eve.tests import TestBase -from eve.tests.test_settings import (MONGO1_DBNAME, MONGO1_PASSWORD, - MONGO1_USERNAME, MONGO_HOST, MONGO_PORT) +from tests import TestBase +from tests.test_settings import ( + MONGO1_DBNAME, + MONGO1_PASSWORD, + MONGO1_USERNAME, + MONGO_HOST, + MONGO_PORT, +) class TestPyMongo(TestBase): diff --git a/eve/tests/test_io/media.py b/tests/test_io/media.py similarity index 99% rename from eve/tests/test_io/media.py rename to tests/test_io/media.py index 88f05f052..9c76b217a 100644 --- a/eve/tests/test_io/media.py +++ b/tests/test_io/media.py @@ -7,7 +7,7 @@ from eve import ETAG, ISSUES, STATUS, STATUS_ERR, STATUS_OK from eve.io.media import MediaStorage from eve.io.mongo import GridFSMediaStorage -from eve.tests import MONGO_DBNAME, TestBase +from tests import MONGO_DBNAME, TestBase class TestMediaStorage(TestCase): diff --git a/eve/tests/test_io/mongo.py b/tests/test_io/mongo.py similarity index 99% rename from eve/tests/test_io/mongo.py rename to tests/test_io/mongo.py index 45f8e03ca..1d0d3cf8a 100644 --- a/eve/tests/test_io/mongo.py +++ b/tests/test_io/mongo.py @@ -9,8 +9,8 @@ from eve.io.mongo import Mongo, MongoJSONEncoder, Validator from eve.io.mongo.parser import ParseError, parse -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME +from tests import TestBase +from tests.test_settings import MONGO_DBNAME class TestPythonParser(TestCase): diff --git a/eve/tests/test_io/multi_mongo.py b/tests/test_io/multi_mongo.py similarity index 97% rename from eve/tests/test_io/multi_mongo.py rename to tests/test_io/multi_mongo.py index 4fcf3caba..ba37a83f8 100644 --- a/eve/tests/test_io/multi_mongo.py +++ b/tests/test_io/multi_mongo.py @@ -9,10 +9,15 @@ import eve from eve.auth import BasicAuth -from eve.tests import TestBase -from eve.tests.test_settings import (MONGO1_DBNAME, MONGO1_PASSWORD, - MONGO1_USERNAME, MONGO_DBNAME, MONGO_HOST, - MONGO_PORT) +from tests import TestBase +from tests.test_settings import ( + MONGO1_DBNAME, + MONGO1_PASSWORD, + MONGO1_USERNAME, + MONGO_DBNAME, + MONGO_HOST, + MONGO_PORT, +) class TestMultiMongo(TestBase): diff --git a/eve/tests/test_logging.py b/tests/test_logging.py similarity index 95% rename from eve/tests/test_logging.py rename to tests/test_logging.py index 47b82613e..8423cae76 100644 --- a/eve/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,6 +1,6 @@ from testfixtures import log_capture -from eve.tests import TestBase +from . import TestBase class TestUtils(TestBase): diff --git a/eve/tests/test_prefix.py b/tests/test_prefix.py similarity index 100% rename from eve/tests/test_prefix.py rename to tests/test_prefix.py diff --git a/eve/tests/test_prefix_version.py b/tests/test_prefix_version.py similarity index 100% rename from eve/tests/test_prefix_version.py rename to tests/test_prefix_version.py diff --git a/eve/tests/test_settings.py b/tests/test_settings.py similarity index 100% rename from eve/tests/test_settings.py rename to tests/test_settings.py diff --git a/eve/tests/test_settings_env.py b/tests/test_settings_env.py similarity index 100% rename from eve/tests/test_settings_env.py rename to tests/test_settings_env.py diff --git a/eve/tests/test_version.py b/tests/test_version.py similarity index 100% rename from eve/tests/test_version.py rename to tests/test_version.py diff --git a/eve/tests/utils.py b/tests/utils.py similarity index 98% rename from eve/tests/utils.py rename to tests/utils.py index 3c8858f26..4c4a66d22 100644 --- a/eve/tests/utils.py +++ b/tests/utils.py @@ -6,10 +6,21 @@ from bson.json_util import dumps -from eve.tests import TestBase -from eve.utils import (config, date_to_str, debug_error_message, document_etag, - extract_key_values, import_from_string, parse_request, - querydef, str_to_date, validate_filters, weak_date) +from eve.utils import ( + config, + date_to_str, + debug_error_message, + document_etag, + extract_key_values, + import_from_string, + parse_request, + querydef, + str_to_date, + validate_filters, + weak_date, +) + +from . import TestBase class TestUtils(TestBase): @@ -292,7 +303,7 @@ def test_import_from_string(self): self.assertEqual(dt, datetime) -class DummyEvent(): +class DummyEvent: """ Even handler that records the call parameters and asserts a check diff --git a/eve/tests/versioning.py b/tests/versioning.py similarity index 99% rename from eve/tests/versioning.py rename to tests/versioning.py index 150bd62a7..6ab3bcea3 100644 --- a/eve/tests/versioning.py +++ b/tests/versioning.py @@ -6,9 +6,10 @@ from bson import ObjectId from eve import ETAG, STATUS, STATUS_OK -from eve.tests import TestBase -from eve.tests.test_settings import MONGO_DBNAME -from eve.tests.utils import DummyEvent + +from . import TestBase +from .test_settings import MONGO_DBNAME +from .utils import DummyEvent class TestVersioningBase(TestBase): diff --git a/tox.ini b/tox.ini index c69ad3831..50d9e902a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist=py3{11,10,9,8,7},pypy3{8,7},linting [testenv] extras=tests -commands=py.test eve {posargs} +commands=pytest tests {posargs} [testenv:linting] skipsdist = True From 3a4a14072d71a0679360acdeabda3a076d90bca8 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 10 Jul 2023 09:05:04 +0200 Subject: [PATCH 770/821] changelog for #1506 --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c056427c8..e5e234e72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Fix: the distribution package should not include the test suite (`#1506`_) - Python 3.11 added to the CI matrix. - .readthedocs.yml upgraded to V2. +.. _`#1506`: https://github.com/pyeve/eve/issues/1506 + + Version v2.1.0 -------------- From dfce5710bbf9c807209c773775bc09fa1b34d387 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Mon, 10 Jul 2023 09:05:49 +0200 Subject: [PATCH 771/821] Guillaume Le Pape --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d12e9fcf1..359eba26a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -69,6 +69,7 @@ Patches and Contributions - Giorgos Margaritis - Gonéri Le Bouder - Grisha K. +- Guillaume Le Pape - Guillaume Royer - Gustavo Vargas - Hamdy From 44bef32af5b6c503be159ac1aafe6f0f81e3c381 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 11 Jun 2024 12:09:19 +0200 Subject: [PATCH 772/821] datetime.utcnow -> datetime.now(UTC) Fixes deprecation warnings in Python 3.12 --- eve/methods/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index ed3d79759..b81ae7306 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -14,7 +14,7 @@ import time from collections import Counter from copy import copy -from datetime import datetime, timezone +from datetime import datetime, UTC from functools import wraps import simplejson as json @@ -1531,4 +1531,4 @@ def oplog_push(resource, document, op, id=None): def utcnow(): - return datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) + return datetime.now(UTC).replace(microsecond=0) From 3c87100364513baea896f2daa17aea9b55124cfb Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 26 Jul 2024 13:55:45 +0000 Subject: [PATCH 773/821] drop eol python; make sure we use timezone.utc --- eve/methods/common.py | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index b81ae7306..f8d46b688 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -14,7 +14,7 @@ import time from collections import Counter from copy import copy -from datetime import datetime, UTC +from datetime import datetime, timezone from functools import wraps import simplejson as json @@ -1531,4 +1531,4 @@ def oplog_push(resource, document, op, id=None): def utcnow(): - return datetime.now(UTC).replace(microsecond=0) + return datetime.now(timezone.utc).replace(microsecond=0) diff --git a/tox.ini b/tox.ini index 50d9e902a..c34787e03 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py3{11,10,9,8,7},pypy3{8,7},linting +envlist=py3{12,11,10,9},pypy3{10,9},linting [testenv] extras=tests From d2f4b8cd0faef203b28399c8a63871c67b6543e3 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 29 Aug 2024 17:51:42 +0200 Subject: [PATCH 774/821] Bret Curtis --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 359eba26a..2ea09fb77 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,6 +28,7 @@ Patches and Contributions - Ben Demaree - Bjorn Andersson - Brad P. Crochet +- Bret Curtis - Brian Mego - Bryan Cattle - Carl George From d9366442d685ad0c97b8a06cbcf573de2633ea39 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 29 Aug 2024 17:24:43 +0200 Subject: [PATCH 775/821] update CI python matrix; drop obsolete pythons --- .github/workflows/ci.yml | 5 ++--- CHANGES.rst | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51c033e8e..fe3fd1a45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,11 @@ jobs: strategy: matrix: include: + - { name: '3.12', python: '3.12', os: ubuntu-20.04, tox: py312, mongodb: '4.4', redis: '6' } - { name: '3.11', python: '3.11', os: ubuntu-20.04, tox: py311, mongodb: '4.4', redis: '6' } - { name: '3.10', python: '3.10', os: ubuntu-20.04, tox: py310, mongodb: '4.4', redis: '6' } - { name: '3.9', python: '3.9', os: ubuntu-20.04, tox: py39, mongodb: '4.4', redis: '6' } - - { name: '3.8', python: '3.8', os: ubuntu-20.04, tox: py38, mongodb: '4.4', redis: '6' } - - { name: '3.7', python: '3.7', os: ubuntu-20.04, tox: py37, mongodb: '4.4', redis-version: '6' } - - { name: 'PyPy', python: 'pypy-3.7', os: ubuntu-20.04, tox: pypy37, mongodb: '4.4', redis: '6' } + - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-20.04, tox: pypy310, mongodb: '4.4', redis: '6' } steps: - uses: actions/checkout@v2 diff --git a/CHANGES.rst b/CHANGES.rst index e5e234e72..3c7f03995 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,10 +6,15 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Fix: deprecation warnings in Python 3.12 (`#1526`_) - Fix: the distribution package should not include the test suite (`#1506`_) +- Python 3.12 added to the CI matrix. - Python 3.11 added to the CI matrix. +- Python 3.9 support dropped. +- Python 3.8 support dropped. - .readthedocs.yml upgraded to V2. +.. _`#1526`: https://github.com/pyeve/eve/issues/1526 .. _`#1506`: https://github.com/pyeve/eve/issues/1506 From e6e10ec4625aef909e206e0b7da98610c5bf90cf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 29 Aug 2024 17:32:18 +0200 Subject: [PATCH 776/821] update CI actions --- .github/workflows/ci.yml | 4 ++-- CHANGES.rst | 8 ++++---- CONTRIBUTING.rst | 8 ++++---- docs/index.rst | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe3fd1a45..01acd32d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-20.04, tox: pypy310, mongodb: '4.4', redis: '6' } steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - uses: supercharge/mongodb-github-action@1.3.0 diff --git a/CHANGES.rst b/CHANGES.rst index 3c7f03995..6728f5771 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,10 +8,10 @@ In Development - Fix: deprecation warnings in Python 3.12 (`#1526`_) - Fix: the distribution package should not include the test suite (`#1506`_) -- Python 3.12 added to the CI matrix. -- Python 3.11 added to the CI matrix. -- Python 3.9 support dropped. -- Python 3.8 support dropped. +- Python 3.12 support. +- Python 3.11 support. +- Python 3.9 dropped. +- Python 3.8 dropped. - .readthedocs.yml upgraded to V2. .. _`#1526`: https://github.com/pyeve/eve/issues/1526 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7449f3d66..66c162511 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -111,12 +111,12 @@ Start coding Running the tests ~~~~~~~~~~~~~~~~~ -You should have Python 3.7+ available in your system. Now +You should have Python 3.9+ available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py37,py38 + $ tox -e linting,py310,py39 -This command will run tests via the "tox" tool against Python 3.7 and 3.8 and +This command will run tests via the "tox" tool against Python 3.10 and 3.9 and also perform "lint" coding-style checks. You can pass different options to ``tox``. For example, to run tests on Python @@ -131,7 +131,7 @@ Or to only run tests in a particular test module on Python 3.6:: CI will run the full suite when you submit your pull request. The full test suite takes a long time to run because it tests multiple combinations of -Python and dependencies. You need to have Python 3.7, 3.8, 3.9, 3.10 and PyPy +Python and dependencies. You need to have Python 3.9, 3.10, 3.11, 3.12 and PyPy installed to run all of the environments. Then run:: tox diff --git a/docs/index.rst b/docs/index.rst index cc749fbda..b92053e50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ Eve is powered by Flask_ and Cerberus_ and it offers native support for MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is provided by community extensions_. -The codebase is thoroughly tested under Python 3.7+, and PyPy. +The codebase is thoroughly tested under Python 3.9+, and PyPy. Eve is Simple ------------- From ea6158cf4905de5f2b4f86fb130c9385e9ba37fc Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Aug 2024 10:24:38 +0200 Subject: [PATCH 777/821] setup.py: add Python 3.12; drop 3.7 and 3.8 --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 319de7056..627f94e89 100755 --- a/setup.py +++ b/setup.py @@ -56,11 +56,10 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", From 8ec3be828fdbcd4710069ab0bf71f796efe40d28 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 30 Aug 2024 10:27:30 +0200 Subject: [PATCH 778/821] pin sphinx-alabaster theme to 0.7.13 --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index f90e0a454..08338de7a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # requirements to build documentation Sphinx<2.0 sphinxcontrib-issuetracker -alabaster +alabaster==0.7.13 doc8 eve jinja2<3.1.0 From 63bd4aef580c8530eab3545b917fb5dd88af89df Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 Oct 2024 08:56:51 +0200 Subject: [PATCH 779/821] bump version to 2.2 --- CHANGES.rst | 6 ++++++ eve/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6728f5771..71c1edae4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- *hic sunt leones* + +Version v2.2 +------------ + +Released on Mar 14, 2023. - Fix: deprecation warnings in Python 3.12 (`#1526`_) - Fix: the distribution package should not include the test suite (`#1506`_) - Python 3.12 support. diff --git a/eve/__init__.py b/eve/__init__.py index daf6ea305..84f2d5092 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.1.0" +__version__ = "2.2.0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 172f3f251bc34d7c59cd72c9b67873b99ba0f486 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 15 Oct 2024 09:27:56 +0200 Subject: [PATCH 780/821] changelog fix --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 71c1edae4..32622cbbc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,7 +11,8 @@ In Development Version v2.2 ------------ -Released on Mar 14, 2023. +Released on Oct 15, 2024. + - Fix: deprecation warnings in Python 3.12 (`#1526`_) - Fix: the distribution package should not include the test suite (`#1506`_) - Python 3.12 support. From a46e8032249f4fa4998404c418b39a626ab74139 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 26 Feb 2025 11:03:07 +0100 Subject: [PATCH 781/821] CI: run tests on ubuntu-latest --- .github/workflows/ci.yml | 24 ++++++++++++++++-------- CHANGES.rst | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01acd32d1..9592736d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,18 @@ jobs: strategy: matrix: include: - - { name: '3.12', python: '3.12', os: ubuntu-20.04, tox: py312, mongodb: '4.4', redis: '6' } - - { name: '3.11', python: '3.11', os: ubuntu-20.04, tox: py311, mongodb: '4.4', redis: '6' } - - { name: '3.10', python: '3.10', os: ubuntu-20.04, tox: py310, mongodb: '4.4', redis: '6' } - - { name: '3.9', python: '3.9', os: ubuntu-20.04, tox: py39, mongodb: '4.4', redis: '6' } - - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-20.04, tox: pypy310, mongodb: '4.4', redis: '6' } + - { name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312, mongodb: '5.0', redis: '6' } + - { name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311, mongodb: '5.0', redis: '6' } + - { name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310, mongodb: '5.0', redis: '6' } + - { name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39, mongodb: '5.0', redis: '6' } + - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-latest, tox: pypy310, mongodb: '4.4', redis: '6' } steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - uses: supercharge/mongodb-github-action@1.3.0 + - uses: supercharge/mongodb-github-action@1.12.0 with: mongodb-version: ${{ matrix.mongodb }} - uses: supercharge/redis-github-action@1.2.0 @@ -34,8 +34,16 @@ jobs: python -m site python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade virtualenv tox tox-gh-actions - - name: Start mongo ${{ matrix.mongodb-version }} + - name: 🍃 Install mongosh run: | - mongo eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' + sudo apt-get update + sudo apt-get install -y wget gnupg + wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list + sudo apt-get update + sudo apt-get install -y mongodb-mongosh + - name: Start mongo ${{ matrix.mongodb }} + run: | + mongosh eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' - name: Run tox targets for ${{ matrix.python }} run: tox -e ${{ matrix.tox }} diff --git a/CHANGES.rst b/CHANGES.rst index 32622cbbc..feb7c6586 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt leones* +- CI: run tests on ubuntu-latest, as ubuntu-20.04 is being decommissioned by GitHub Actions. Version v2.2 ------------ From 125271f86c453e1a1d2f3f17fa853c9567178cfd Mon Sep 17 00:00:00 2001 From: Pablo Parada Date: Wed, 30 Oct 2024 14:57:30 +0100 Subject: [PATCH 782/821] fix: type-checking Eve dynamic attrs --- eve/flaskapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 5cb8791d5..630ca7bec 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -14,6 +14,7 @@ import os import sys import warnings +from typing import TYPE_CHECKING from events import Events from flask import Flask @@ -1102,3 +1103,6 @@ def __call__(self, environ, start_response): "HTTP_X_HTTP_METHOD_OVERRIDE", environ["REQUEST_METHOD"] ).upper() return super().__call__(environ, start_response) + + if TYPE_CHECKING: + def __setattr__(self, name, value): ... From 0549152989a3f6f281b7326197dff293dfab2567 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 26 Feb 2025 14:54:28 +0100 Subject: [PATCH 783/821] Pablo Parada --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 2ea09fb77..cdf4ad1d9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -152,6 +152,7 @@ Patches and Contributions - Ondrej Slinták - Or Neeman - Orange Tsai +- Pablo Parada - Pahaz Blinov - Patricia Ramos - Patrick Decat From 6d3719092c70ba5805c3d1a6ea092dd62acb76a4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 26 Feb 2025 14:54:42 +0100 Subject: [PATCH 784/821] changelog for #1541 --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index feb7c6586..61c911d59 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- fix: allow for type-checking of Eve's dynamic attrs (`#1541`_) - CI: run tests on ubuntu-latest, as ubuntu-20.04 is being decommissioned by GitHub Actions. +.. _`#1541`: https://github.com/pyeve/eve/pull/1541 + Version v2.2 ------------ From eef37a49758c91d4703a6eab886c48dc3adebe6f Mon Sep 17 00:00:00 2001 From: Svante Bengtson Date: Wed, 26 Feb 2025 10:45:44 +0100 Subject: [PATCH 785/821] Correct on_deleted_resource call signature --- docs/features.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index 5ae4c6afb..753954719 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1419,10 +1419,10 @@ Let's see an overview of what events are available: | | | || ``def event(originals, lookup)`` | | | +------+--------------------------------------------------+ | | |After || ``on_deleted_resource`` | -| | | || ``def event(resource_name, item)`` | +| | | || ``def event(resource_name)`` | | | | +--------------------------------------------------+ | | | || ``on_deleted_resource_`` | -| | | || ``def event(item)`` | +| | | || ``def event()`` | +-------+--------+------+--------------------------------------------------+ From a7813bfe2763af1c7076c17324fb5308a01cb332 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 26 Feb 2025 14:59:25 +0100 Subject: [PATCH 786/821] Svante Bengtson --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cdf4ad1d9..eb177ea0f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -192,6 +192,7 @@ Patches and Contributions - Stanislav Heller - Stefaan Ghysels - Stratos Gerakakis +- Svante Bengtson - Sybren A. Stüvel - Tadej Magajn - Tano Abeleyra From f1b48b7395113a3285ee9f9976e752a38a765a43 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 26 Feb 2025 15:00:38 +0100 Subject: [PATCH 787/821] changelog for #1547 --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 61c911d59..45d591f3f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,11 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- fix: correct `on_deleted_resource` call signature (docs) (`#1547`_) - fix: allow for type-checking of Eve's dynamic attrs (`#1541`_) - CI: run tests on ubuntu-latest, as ubuntu-20.04 is being decommissioned by GitHub Actions. +.. _`#1547`: https://github.com/pyeve/eve/pull/1547 .. _`#1541`: https://github.com/pyeve/eve/pull/1541 Version v2.2 From 192051b2ff2191a039bd86c397be6bf529562345 Mon Sep 17 00:00:00 2001 From: Alexander Enrique Urieles Nieto Date: Sun, 18 May 2025 12:22:20 +0200 Subject: [PATCH 788/821] Caches field_definition calls to improve build_response_document performance --- eve/methods/common.py | 4 ++-- tests/__init__.py | 6 ++++++ tests/methods/delete.py | 3 ++- tests/methods/get.py | 4 ++++ tests/versioning.py | 3 +++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/eve/methods/common.py b/eve/methods/common.py index f8d46b688..5cdfd27e3 100644 --- a/eve/methods/common.py +++ b/eve/methods/common.py @@ -15,7 +15,7 @@ from collections import Counter from copy import copy from datetime import datetime, timezone -from functools import wraps +from functools import cache, wraps import simplejson as json from bson.dbref import DBRef @@ -699,6 +699,7 @@ def resolve_resource_projection(document, resource): del document[field] +@cache def field_definition(resource, chained_fields): """Resolves query string to resource with dot notation like 'people.address.city' and returns corresponding field definition @@ -747,7 +748,6 @@ def resolve_data_relation_links(document, resource): related_dict = {} for field in resource_def.get("schema", {}): - field_def = field_definition(resource, field) if "data_relation" not in field_def: continue diff --git a/tests/__init__.py b/tests/__init__.py index 8c23e3575..b86067ff7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -12,6 +12,7 @@ import eve from eve import ETAG, ISSUES +from eve.methods.common import field_definition from .test_settings import ( DOMAIN, MONGO_DBNAME, @@ -106,6 +107,8 @@ def setUp(self, settings_file=None, url_converters=None): self.domain = self.app.config["DOMAIN"] + self.clearSchemaCache() + def tearDown(self): del self.app self.dropDB() @@ -394,6 +397,9 @@ def dropDB(self): self.connection.drop_database(MONGO_DBNAME) self.connection.close() + def clearSchemaCache(self): + field_definition.cache_clear() + class TestBase(TestMinimal): def setUp(self, url_converters=None): diff --git a/tests/methods/delete.py b/tests/methods/delete.py index b971fbbab..c7a66d091 100644 --- a/tests/methods/delete.py +++ b/tests/methods/delete.py @@ -439,8 +439,9 @@ def test_softdeleted_embedded_doc(self): invoices = self.domain["invoices"] invoices["embedding"] = True invoices["schema"]["person"]["data_relation"]["embeddable"] = True - embedded = '{"person": 1}' + self.clearSchemaCache() + embedded = '{"person": 1}' r = self.test_client.get(self.invoice_id_url + "?embedded=%s" % embedded) data, status = self.parse_response(r) self.assert200(status) diff --git a/tests/methods/get.py b/tests/methods/get.py index 480528b99..8952fb956 100644 --- a/tests/methods/get.py +++ b/tests/methods/get.py @@ -927,6 +927,7 @@ def test_get_embedded(self): # Test that global setting applies even if field is set to embedded invoices["embedding"] = False + self.clearSchemaCache() r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) @@ -934,6 +935,7 @@ def test_get_embedded(self): # Test that it works invoices["embedding"] = True + self.clearSchemaCache() r = self.test_client.get("%s/%s" % (invoices["url"], "?embedded=%s" % embedded)) self.assert200(r.status_code) content = json.loads(r.get_data()) @@ -966,6 +968,7 @@ def test_get_embedded(self): "type": "objectid", "data_relation": {"resource": "contacts", "embeddable": True}, } + self.clearSchemaCache() # Test that it ignores embeddable field that is missing from document embedded = '{"missing-field": 1}' @@ -976,6 +979,7 @@ def test_get_embedded(self): # Test default fields to be embedded invoices["embedded_fields"] = ["person"] + self.clearSchemaCache() r = self.test_client.get("%s/" % invoices["url"]) self.assert200(r.status_code) content = json.loads(r.get_data()) diff --git a/tests/versioning.py b/tests/versioning.py index 6ab3bcea3..a5e1c028c 100644 --- a/tests/versioning.py +++ b/tests/versioning.py @@ -33,6 +33,7 @@ def tearDown(self): self.connection.close() def enableVersioning(self, partial=False): + self.clearSchemaCache() del self.domain["contacts"]["schema"]["title"]["default"] del self.domain["contacts"]["schema"]["dependency_field1"]["default"] del self.domain["contacts"]["schema"]["unsetted_default_value_field"]["default"] @@ -52,6 +53,7 @@ def enableVersioning(self, partial=False): def enableDataVersionRelation( self, embeddable=True, custom_field=None, custom_field_type="string" ): + self.clearSchemaCache() field = { "type": "dict", "schema": {self.app.config["VERSION"]: {"type": "integer"}}, @@ -69,6 +71,7 @@ def enableDataVersionRelation( self.domain["invoices"]["schema"]["person"] = field def enableSoftDelete(self): + self.clearSchemaCache() self.app.config["SOFT_DELETE"] = True domain = copy.copy(self.domain) for resource, settings in domain.items(): From 1c05946f6fa558548fea4699e77b14a36c9326ec Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 3 Jun 2025 11:41:20 +0200 Subject: [PATCH 789/821] changelog for #1553 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 45d591f3f..67546001f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,10 +6,12 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- fix: correct `on_deleted_resource` call signature (docs) (`#1547`_) +- fix: ``field_definition`` impacts negatively the performance of ``build_response_document`` (`#1552`_) +- fix: correct ``on_deleted_resource`` call signature (docs) (`#1547`_) - fix: allow for type-checking of Eve's dynamic attrs (`#1541`_) - CI: run tests on ubuntu-latest, as ubuntu-20.04 is being decommissioned by GitHub Actions. +.. _`#1552`: https://github.com/pyeve/eve/issues/1552 .. _`#1547`: https://github.com/pyeve/eve/pull/1547 .. _`#1541`: https://github.com/pyeve/eve/pull/1541 From 201c68ae8c1bfe64757e00479e55c501bd26ac99 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 3 Jun 2025 11:41:24 +0200 Subject: [PATCH 790/821] Alexander Urieles --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index eb177ea0f..16b52de35 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,6 +17,7 @@ Patches and Contributions - Alexander Dietmüller - Alexander Hendorf - Alexander Miskaryan +- Alexander Urieles - Amedeo Bussi - Andreas Røssland - Andrés Martano From 21f8b6db18018afbab591224da2bd5e486a7a404 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 3 Jun 2025 15:00:09 +0200 Subject: [PATCH 791/821] bump version to 2.2.1 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 67546001f..ec05dd551 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- *hic sunt leones* + +Version v2.2.1 +-------------- + +Released on June 3, 2025. + - fix: ``field_definition`` impacts negatively the performance of ``build_response_document`` (`#1552`_) - fix: correct ``on_deleted_resource`` call signature (docs) (`#1547`_) - fix: allow for type-checking of Eve's dynamic attrs (`#1541`_) diff --git a/eve/__init__.py b/eve/__init__.py index 84f2d5092..d47755c5a 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.0" +__version__ = "2.2.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 834c994f4ec38930b8708f897517dfcd534a3002 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 15:09:43 +0200 Subject: [PATCH 792/821] chore: add Invoicetronic as the main sponsor for the project --- CHANGES.rst | 4 +++- README.rst | 18 ++++++++++++----- docs/_static/backers/blokt.png | Bin 2249 -> 0 bytes docs/_static/invoicetronic.svg | 35 +++++++++++++++++++++++++++++++++ docs/funding.rst | 31 +++++++++++------------------ docs/index.rst | 7 +++++++ 6 files changed, 70 insertions(+), 25 deletions(-) delete mode 100644 docs/_static/backers/blokt.png create mode 100644 docs/_static/invoicetronic.svg diff --git a/CHANGES.rst b/CHANGES.rst index ec05dd551..9801ee104 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt leones* +- `Invoicetronic`_ is now the main sponsor for this project. + +.. _Invoicetronic: https://invoicetronic.com/en/ Version v2.2.1 -------------- diff --git a/README.rst b/README.rst index a6eee1f0c..80b3ef2e7 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,12 @@ Features * MongoDB and SQL Support * Powered by Flask +License +------- +Eve is a `Nicola Iarocci`_ open source project, +distributed under the `BSD license +`_. + Funding ------- Eve REST framework is a open source, collaboratively funded project. If you run @@ -92,11 +98,13 @@ helped you in your work or personal projects. Every single sign-up makes a significant impact towards making Eve possible. To learn more, check out our `funding page`_. -License -------- -Eve is a `Nicola Iarocci`_ open source project, -distributed under the `BSD license -`_. +Sponsored by +------------ + +.. image:: docs/_static/invoicetronic.svg + :target: https://invoicetronic.com/en/ + :width: 50 % + :alt: Invoicetronic is the leading API for electronic invoicing in Italy .. _`Nicola Iarocci`: http://nicolaiarocci.com .. _`funding page`: http://python-eve.org/funding.html diff --git a/docs/_static/backers/blokt.png b/docs/_static/backers/blokt.png deleted file mode 100644 index 5f1be4dcf85831328a084bebe3247e668b547ccb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2249 zcmV;)2sZbLP)-$00004XF*Lt006O% z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)x010qNS#tmY3labT3lag+-G2N400=EfL_t(|+U=TctdzwS z$A7%|R$41IskXL+z!uOCn9arP7dt}?G-_Y8rq`tHG{vZw8e62d0vADSl^RqKR0!qT zDusc*+z&OWrACP9Oe|(omNh0#tliwkCT$_L#gtmBAoRnWWuLqEdD*vnp|bzv=AL=x z%z0*K|2yZ*IRjmE(M1O0p1 z)kDEn<9s7=Sr_eLDzG5oZ)(z@QN)s6ry3l;1>6EWAngl)&1ti+TKxq0tu#Mxoc~P3 z_9WoZi2BkrTSCIB%lnPhY8kLw+OvRfj{7oq(GcQX;d@Fe184z6R6(TPKE*JU7R;v=}*cg$kf&N3lZG@BRTI0O6S{*sv3)w0p=$?_G zHay;kfp#&1SjPE0VTS^E)oQgV(tSHn7I_;mC&{SQ>Y#CcJFp12+&I6;YPB8suBf~T zd?#&U#`&d@cl^+5bwF(Y)i{3-fv^`6cEDR`4SRF>LY7WR{sxociDn=%%L+1FNzAya zI3#mvIT)7{?cNlXL3z*K3{&;%x3D^|dRO>(ME-WP!)o<1uw9z7jq`VAytE(qm&hB$ z4L&IC6;`W%Bn>pqf0&RzgW!%=*Qsow7SJz}AH-Op2k4VU%-UH}k;@mR627cnnjlpj zdSxy>tqhiNKTlB^tdxt3+g-F%#4^r*8Ms=S&swdXi_u>Rye;zDq(I&{KUv(kG_ znkCjC@rle{+9y#n7zUEQ)O8)0OEs=*hEyeBA?bj38-OnYGl9<;=jU3j zUP&-^H^H?5yb8QdC{mw{=u9Bg^uBJjT5O#Eh1kE|I6q$|{0zYrN5`EYWcKGvdkK)3 zkY!|M|Na&hd?+pmmVk6ziIo(L=kkS4oOsjRX6#N?DHo3b`!WtUiKU{+aLUCk0e(dw z$reHxWdx)sy_P_nzX3f~tHhfL#`&Gb`R`k;ZZghS3AEd4_3=3Sk#QRdG1QgD`3J04 z_r~b-0@q7(kJW0Z17E<|hT%@SzbH-x?^dfl1Y$vmS#FNe-#{%cT9HAj%K!uP8|N<~ z1pWXYGtS>fI#vOPL|$&3pH^pk3bjP*G3rjRT{MgtxqM;USQxHPH$=-Zk3>r*#(4$Y zB+Y|XtDPCPt0%Sdf%#Ueb->@{*-GR5CzA#WVXJQLTtd<|io1bNh`iZq_1ai0&ie>M zgkpXi`o@A>BNC)mtJjS48wknEX@t0?>V^g*s<)*Xts`Lx*dua3;hxrcz!SiYNj9uj zPaEg&0ImXht3_wy{LILl4lz2%)eP1p#4zo~SKD&=LiA8DR4Eq+Q}las`NE2XzP55_ zf)asTN%^b3T)sdHZgJH5Rm#PM5MR<+LpW(&N^nDu0M`JsrGJOjs+{yD+2x*yJo=W^ z>RkY1)BXRvk6`|#eS|!bMfuB+<#k@gw;de1GSL9t*tC#Bfmqz5& z#HNg`e$XD{{0>6;0-$WQ+R^L+w}~j3S~;zCvZmQ|c}Yl6pr2RHA^+2oDe&hUZ;48pfeeGt!o5c|#;XtKaEamcreKNO3F+|vzI*}-CW@kBtO1U^F$93R*TZ(fRZfiyqFsXH`B!JOvw$+BR!bT!Vsa@+wyG(iKWtrIb!Fkl#2s|ovz_| zXWe<u1h18;6cKHU>f1xdz9oSWArBwT*%h<3#p)%;Xd8-=%R}*y6B>dF1qOA%*X!# Xa-GM;{B+vp00000NkvXXu0mjfb>=$* diff --git a/docs/_static/invoicetronic.svg b/docs/_static/invoicetronic.svg new file mode 100644 index 000000000..faa937c95 --- /dev/null +++ b/docs/_static/invoicetronic.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/funding.rst b/docs/funding.rst index a5a1e98e2..43ddd02b7 100644 --- a/docs/funding.rst +++ b/docs/funding.rst @@ -32,6 +32,12 @@ You can support Eve development by pledging on GitHub, Patreon, or PayPal. - `Become a Backer on Patreon `_ - `Donate via PayPal `_ (one time) +Backers +~~~~~~~ +Backers who actively support Eve and Cerberus development: + +- Gabriel Wainer + Eve Course at TalkPython Training --------------------------------- There is a 5 hours-long Eve course available for you at the fine TalkPython @@ -40,26 +46,13 @@ course will directly support the project. - `Take the Eve Course at TalkPython Training `_ -Custom Sponsorship and Consulting ---------------------------------- -If you are a business that is building core products using Eve, I am also -open to conversations regarding custom sponsorship / consulting arrangements. -Just `get in touch`_ with me. +Sponsored by +------------ +.. image:: _static/invoicetronic.svg + :target: https://invoicetronic.com/en/ + :width: 50 % + :alt: Invoicetronic is the leading API for electronic invoicing in Italy .. _`get in touch`: mailto:nicola@nicolaiarocci.com .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course -Backers -~~~~~~~ -Backers who actively support Eve and Cerberus development: - -- Gabriel Wainer -- Jon Kelled - -Generous Backers -~~~~~~~~~~~~~~~~ -Generous backers who actively support Eve and Cerberus development: - -.. image:: _static/backers/blokt.png - :target: http://blokt.com/guides/best-vpn - :alt: Blokt Crypto & Privacy diff --git a/docs/index.rst b/docs/index.rst index b92053e50..ec1adfde2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,6 +73,13 @@ You can support Eve development by pledging on GitHub, Patreon, or PayPal. - `Become a Backer on Patreon `_ - `Donate via PayPal `_ (one time) +Sponsored by +------------ +.. image:: _static/invoicetronic.svg + :target: https://invoicetronic.com/en/ + :width: 50 % + :alt: Invoicetronic is the leading API for electronic invoicing in Italy + .. toctree:: :hidden: From f5aed3ca536c5263d1b2ef4ce64c4b2862258d84 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 15:46:17 +0200 Subject: [PATCH 793/821] drop the twitter reference from website --- CHANGES.rst | 1 + docs/_templates/sidebarintro.html | 13 ------------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9801ee104..3cce60dc1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Drop the twitter reference from the docs, as the author left that platform. - `Invoicetronic`_ is now the main sponsor for this project. .. _Invoicetronic: https://invoicetronic.com/en/ diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 3049fb1b7..27dfaf7bc 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,16 +1,3 @@ -

    Stay Informed

    -

    Receive updates on new releases and upcoming projects.

    - -

    - -

    - -

    - -

    Join Mailing List.

    -

    Eve Course

    This course will teach you how to build RESTful services with Eve and MongoDB.

    The teacher is the project creator and maintainer.

    From f0ef1a76cf43545e7fc8339058d81f9d2dce72f4 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 15:48:58 +0200 Subject: [PATCH 794/821] drop obsolete reference links from the docs --- CHANGES.rst | 1 + docs/_templates/sidebarintro.html | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3cce60dc1..a4d78e46a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- Drop obsolete reference links from the docs. - Drop the twitter reference from the docs, as the author left that platform. - `Invoicetronic`_ is now the main sponsor for this project. diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 27dfaf7bc..9650863d3 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -10,8 +10,6 @@

    Useful Links

    • Eve @ GitHub
    • Eve @ Stack Overflow
    • -
    • Eve @ Google Groups
    • -
    • Eve @ IRC
    • Eve @ PyPI
    • Eve @ Nicola Iarocci
    • Issue Tracker
    • From c39698ca558fa0e3c28ca6183c4f5f29cf0bd430 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 15:58:05 +0200 Subject: [PATCH 795/821] bump version to 2.2.2 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a4d78e46a..846e5ee8b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- *hic sunt leones* + +Version v2.2.2 +-------------- + +Released on August 26, 2025. *Just website updates, no code changes.* + - Drop obsolete reference links from the docs. - Drop the twitter reference from the docs, as the author left that platform. - `Invoicetronic`_ is now the main sponsor for this project. diff --git a/eve/__init__.py b/eve/__init__.py index d47755c5a..f80f2cb28 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.1" +__version__ = "2.2.2" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 0503a9ed5bddd6710170f1ced8d23e6372fed858 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 16:10:17 +0200 Subject: [PATCH 796/821] use remote url link for sponsor link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 80b3ef2e7..3d2e1483c 100644 --- a/README.rst +++ b/README.rst @@ -101,7 +101,7 @@ learn more, check out our `funding page`_. Sponsored by ------------ -.. image:: docs/_static/invoicetronic.svg +.. image:: https://raw.githubusercontent.com/pyeve/eve/refs/heads/master/docs/_static/invoicetronic.svg :target: https://invoicetronic.com/en/ :width: 50 % :alt: Invoicetronic is the leading API for electronic invoicing in Italy From 0245e0cca95dba80ad23b9fb10b39a46112ba87d Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 26 Aug 2025 16:11:54 +0200 Subject: [PATCH 797/821] bump version to 2.2.3 --- CHANGES.rst | 4 ++-- eve/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 846e5ee8b..09347fd9d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,8 +8,8 @@ In Development - *hic sunt leones* -Version v2.2.2 --------------- +Version v2.2.2 and v2.2.3 +------------------------- Released on August 26, 2025. *Just website updates, no code changes.* diff --git a/eve/__init__.py b/eve/__init__.py index f80f2cb28..fa42367d2 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.2" +__version__ = "2.2.3" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 4585a8ec344cab0492a6247049b3dba300bfe538 Mon Sep 17 00:00:00 2001 From: smeng9 Date: Mon, 24 Nov 2025 14:07:32 +0800 Subject: [PATCH 798/821] fix incorrect error when replacing items --- eve/io/mongo/validation.py | 5 ++++- tests/methods/put.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index 549cebebd..4f8ebe36e 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -127,7 +127,10 @@ def _is_value_unique(self, unique, field, value, query): # exclude current document if self.document_id: id_field = resource_config["id_field"] - query[id_field] = {"$ne": self.document_id} + if id_field in query: + query[id_field] = {"$ne": self.document_id, "$eq": query[id_field]} + else: + query[id_field] = {"$ne": self.document_id} # we perform the check on the native mongo driver (and not on # app.data.find_one()) because in this case we don't want the usual diff --git a/tests/methods/put.py b/tests/methods/put.py index 412243414..b9032b2d7 100644 --- a/tests/methods/put.py +++ b/tests/methods/put.py @@ -75,6 +75,16 @@ def test_unique_value(self): r, {"ref": "value '%s' is not unique" % self.alt_ref} ) + def test_unique_idfield(self): + self.domain["products"]["schema"]["sku"]["unique"] = True + response, status = self.get("products?max_results=1") + product = response["_items"][0] + headers = [("If-Match", product[ETAG])] + r, status = self.put( + "products/%s" % product["sku"], data=product, headers=headers + ) + self.assert200(status) + def test_allow_unknown(self): changes = {"unknown": "unknown"} r, status = self.put( From 263bc4815c7ee25885197c30003687c60609316e Mon Sep 17 00:00:00 2001 From: smeng9 Date: Mon, 24 Nov 2025 17:55:22 +0800 Subject: [PATCH 799/821] fix tests --- tests/methods/put.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/methods/put.py b/tests/methods/put.py index b9032b2d7..f5f5312ee 100644 --- a/tests/methods/put.py +++ b/tests/methods/put.py @@ -80,8 +80,13 @@ def test_unique_idfield(self): response, status = self.get("products?max_results=1") product = response["_items"][0] headers = [("If-Match", product[ETAG])] + updated_product = { + "sku": product["sku"], + "title": product["title"], + "parent_product": product["parent_product"], + } r, status = self.put( - "products/%s" % product["sku"], data=product, headers=headers + "products/%s" % product["sku"], data=updated_product, headers=headers ) self.assert200(status) From ecfd9e57a388b9a6d37b91a87c20b56a7ec47aaf Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Dec 2025 15:40:42 +0100 Subject: [PATCH 800/821] changelog for #1560 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 09347fd9d..fea4b3fbe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *hic sunt leones* +- fix: Validation issue when a field is ``id_field`` with ``unique=True`` (`#1559`_) + +.. _`#1559`: https://github.com/pyeve/eve/issues/1559 Version v2.2.2 and v2.2.3 ------------------------- From f4f31201b21004b1da084ae08de9428436f15d1e Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 2 Dec 2025 15:44:05 +0100 Subject: [PATCH 801/821] bump version to 2.2.4 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fea4b3fbe..fd6709e95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development --------------- +- *his sunt leones* + +Version v2.2.4 +-------------- + +Released on December 2, 2025. + - fix: Validation issue when a field is ``id_field`` with ``unique=True`` (`#1559`_) .. _`#1559`: https://github.com/pyeve/eve/issues/1559 diff --git a/eve/__init__.py b/eve/__init__.py index fa42367d2..96d0e174f 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.3" +__version__ = "2.2.4" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 956d7162710de40dd660aa2ab20f17e1daf00725 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Feb 2026 10:12:54 +0100 Subject: [PATCH 802/821] add claude/ to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f681a98af..070b35f42 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ _build pip-wheel-metadata/ !/.eggs/ .venv/ +.claude/ From 6a1fc10584c1f5952365965ae44b180c3717f4fc Mon Sep 17 00:00:00 2001 From: eukaryote Date: Tue, 10 Feb 2026 12:57:30 -0800 Subject: [PATCH 803/821] fix OPTIMIZE_PAGINATION_FOR_SPEED 'next' link - add a test and a fix for the 'next' link incorrectly including a document id in the href when paginating through collections when OPTIMIZE_PAGINATION_FOR_SPEED is true; the next link was being generated in the form "/?page=2" instead of the correct "?page=2". --- eve/methods/get.py | 9 +++++++-- tests/methods/get.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/eve/methods/get.py b/eve/methods/get.py index c380e6180..cf5ad9f02 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -610,8 +610,13 @@ def _pagination_links(resource, req, document_count, document_id=None): # create pagination links if config.DOMAIN[resource]["pagination"]: - # strip any queries from the self link if present - _pagination_link = _links["self"]["href"].split("?")[0] + # For version pagination (all/diffs), use the document self link. + # Otherwise, use the resource (collection) link so that item + # endpoints don't include a document ID in the next/prev/last hrefs. + if document_id and version not in ("all", "diffs"): + _pagination_link = resource_link() + else: + _pagination_link = _links["self"]["href"].split("?")[0] if ( req.page * req.max_results < (document_count or 0) diff --git a/tests/methods/get.py b/tests/methods/get.py index 8952fb956..d5192ee68 100644 --- a/tests/methods/get.py +++ b/tests/methods/get.py @@ -2197,6 +2197,30 @@ def test_getitem_with_custom_idfield(self): response, status = self.get("products", item=sku) self.assertItemResponse(response, status, "products") + def test_getitem_optimize_pagination_next_link(self): + """When OPTIMIZE_PAGINATION_FOR_SPEED is enabled, the ``next`` link on + an item endpoint should use the collection URL, not the document URL. + Regression test for a bug where the next href incorrectly included the + document ID (e.g. ``resource/?page=2`` instead of + ``resource?page=2``). + """ + self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"] = True + + response, status = self.get(self.known_resource, item=self.item_id) + self.assert200(status) + + links = response["_links"] + self.assertIn("next", links) + + next_href = links["next"]["href"] + resource_url = self.domain[self.known_resource]["url"] + self.assertTrue( + next_href.startswith("%s?" % resource_url), + "Expected next href to start with '%s?' but got '%s'" + % (resource_url, next_href), + ) + self.assertNotIn(str(self.item_id), next_href) + class TestHead(TestBase): def test_head_home(self): From 70a7e81d0b6664d4a98497c094a64196d3a037ea Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Feb 2026 10:14:53 +0100 Subject: [PATCH 804/821] Calvin Smith --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 16b52de35..b7268335b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Patches and Contributions - Bret Curtis - Brian Mego - Bryan Cattle +- Calvin Smith - Carl George - Carles Bruguera - Chen Rotem From 1837cde4c7481ef94108c0e6a329d7708052ac8c Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Feb 2026 10:17:36 +0100 Subject: [PATCH 805/821] update changelog for #1567 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fd6709e95..0ee464a72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development --------------- -- *his sunt leones* +- fix: ``OPTIMIZE_PAGINATION_FOR_SPEED`` 'next' link incorrectly included a document id in the href when paginating through collections (`#1567`_) + +.. _`#1567`: https://github.com/pyeve/eve/pull/1567 Version v2.2.4 -------------- From acf2bdac41d32bd9cd6cf9e36807b98d9eaba3a9 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Feb 2026 10:23:17 +0100 Subject: [PATCH 806/821] bump version to 2.2.5 --- CHANGES.rst | 9 ++++++++- eve/__init__.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0ee464a72..114043114 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,14 @@ Changelog Here you can see the full list of changes between each Eve release. In Development ---------------- +-------------- + +- *hic sunt leones* + +Version v2.2.5 +-------------- + +Released on February 11, 2026. - fix: ``OPTIMIZE_PAGINATION_FOR_SPEED`` 'next' link incorrectly included a document id in the href when paginating through collections (`#1567`_) diff --git a/eve/__init__.py b/eve/__init__.py index 96d0e174f..ef22bde38 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.4" +__version__ = "2.2.5" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 2f0ff474469c94b873d8fb4c84eaeac4beffabf0 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Wed, 11 Feb 2026 10:34:15 +0100 Subject: [PATCH 807/821] fix ReadTheDocs build: update Sphinx and docs dependencies Sphinx 1.8.6 imports pkg_resources which was removed from modern setuptools, breaking the RTD build. Update to Sphinx 7.x and remove obsolete dependencies (sphinxcontrib-issuetracker, jinja2 pin). --- docs/conf.py | 5 +---- docs/requirements.txt | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f71553269..ed0add9c6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,8 +15,6 @@ import os import sys -import alabaster - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -31,7 +29,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "alabaster"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -110,7 +108,6 @@ # html_theme_options = {'touch_icon': 'touch-icon.png'} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [alabaster.get_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". diff --git a/docs/requirements.txt b/docs/requirements.txt index 08338de7a..77fc977c4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,5 @@ # requirements to build documentation -Sphinx<2.0 -sphinxcontrib-issuetracker -alabaster==0.7.13 +Sphinx>=7.0,<8.0 +alabaster>=0.7.13,<1.0 doc8 eve -jinja2<3.1.0 From 24e02b9222cb0de1bdfc5636425c8015eaf74e1d Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Wed, 18 Mar 2026 18:05:43 +0100 Subject: [PATCH 808/821] optimize_pagination_for_speed --- eve/flaskapp.py | 1 + eve/methods/get.py | 14 +++++++------- tests/methods/get.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 630ca7bec..36899cd66 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -682,6 +682,7 @@ def _set_resource_defaults(self, resource, settings): "normalize_dotted_fields", self.config["NORMALIZE_DOTTED_FIELDS"] ) settings.setdefault("normalize_on_patch", self.config["NORMALIZE_ON_PATCH"]) + settings.setdefault("optimize_pagination_for_speed", self.config["OPTIMIZE_PAGINATION_FOR_SPEED"]) # empty schemas are allowed for read-only access to resources schema = settings.setdefault("schema", {}) self.set_schema_defaults(schema, settings["id_field"]) diff --git a/eve/methods/get.py b/eve/methods/get.py index cf5ad9f02..c9906ca4f 100644 --- a/eve/methods/get.py +++ b/eve/methods/get.py @@ -230,7 +230,7 @@ def prune_aggregation_stage(d): # add pagination info if config.DOMAIN[resource]["pagination"]: - response[config.META] = _meta_links(req, count) + response[config.META] = _meta_links(resource, req, count) if config.DOMAIN[resource]["hateoas"]: response[config.LINKS] = _pagination_links(resource, req, count) @@ -255,7 +255,7 @@ def _perform_find(resource, lookup): req.if_modified_since = None cursor, count = app.data.find( - resource, req, lookup, perform_count=not config.OPTIMIZE_PAGINATION_FOR_SPEED + resource, req, lookup, perform_count=not config.DOMAIN[resource]["optimize_pagination_for_speed"] ) # If soft delete is enabled, data.find will not include items marked # deleted unless req.show_deleted is True @@ -281,7 +281,7 @@ def _perform_find(resource, lookup): # add pagination info if config.DOMAIN[resource]["pagination"]: - response[config.META] = _meta_links(req, count) + response[config.META] = _meta_links(resource, req, count) # notify registered callback functions. Please note that, should the # functions modify the documents, the last_modified and etag won't be @@ -505,7 +505,7 @@ def getitem_internal(resource, **lookup): resource, req, count, latest_doc[resource_def["id_field"]] ) if config.DOMAIN[resource]["pagination"]: - response[config.META] = _meta_links(req, count) + response[config.META] = _meta_links(resource, req, count) else: response[config.LINKS].update( _pagination_links( @@ -620,7 +620,7 @@ def _pagination_links(resource, req, document_count, document_id=None): if ( req.page * req.max_results < (document_count or 0) - or config.OPTIMIZE_PAGINATION_FOR_SPEED + or config.DOMAIN[resource]["optimize_pagination_for_speed"] ): q = querydef( req.max_results, @@ -689,7 +689,7 @@ def _other_params(args): ) -def _meta_links(req, count): +def _meta_links(resource, req, count): """Reterns the meta links for a paginated query. :param req: parsed request object. @@ -698,6 +698,6 @@ def _meta_links(req, count): .. versionadded:: 0.5 """ meta = {config.QUERY_PAGE: req.page, config.QUERY_MAX_RESULTS: req.max_results} - if config.OPTIMIZE_PAGINATION_FOR_SPEED is False: + if config.DOMAIN[resource]["optimize_pagination_for_speed"] is False: meta["total"] = count return meta diff --git a/tests/methods/get.py b/tests/methods/get.py index d5192ee68..4fb3f42a5 100644 --- a/tests/methods/get.py +++ b/tests/methods/get.py @@ -106,6 +106,20 @@ def test_get_perform_count_on_pagination_disabled(self): self.assertPrevLink(links, 1) self.assertFalse(self.app.config["HEADER_TOTAL_COUNT"] in r.headers) + def test_get_perform_count_on_pagination_disabled_on_resource(self): + self.app.config["DOMAIN"][self.known_resource]["optimize_pagination_for_speed"] = True + + r = self.test_client.get("%s?page=2" % self.known_resource_url) + self.assert200(r.status_code) + + body = json.loads(r.get_data()) + links = body["_links"] + self.assertFalse("last" in links) + self.assertFalse("total" in body["_meta"]) + self.assertNextLink(links, 3) + self.assertPrevLink(links, 1) + self.assertFalse(self.app.config["HEADER_TOTAL_COUNT"] in r.headers) + def test_get_internal_page(self): with self.app.test_request_context(self.known_resource_url): response, _, _, status, _ = get_internal(self.known_resource) @@ -2221,6 +2235,30 @@ def test_getitem_optimize_pagination_next_link(self): ) self.assertNotIn(str(self.item_id), next_href) + def test_getitem_optimize_pagination_next_link_on_resource(self): + """When optimize_pagination_for_speed is enabled, the ``next`` link on + an item endpoint should use the collection URL, not the document URL. + Regression test for a bug where the next href incorrectly included the + document ID (e.g. ``resource/?page=2`` instead of + ``resource?page=2``). + """ + self.app.config["DOMAIN"][self.known_resource]["optimize_pagination_for_speed"] = True + + response, status = self.get(self.known_resource, item=self.item_id) + self.assert200(status) + + links = response["_links"] + self.assertIn("next", links) + + next_href = links["next"]["href"] + resource_url = self.domain[self.known_resource]["url"] + self.assertTrue( + next_href.startswith("%s?" % resource_url), + "Expected next href to start with '%s?' but got '%s'" + % (resource_url, next_href), + ) + self.assertNotIn(str(self.item_id), next_href) + class TestHead(TestBase): def test_head_home(self): From 59f7b600bc2e1a648ec1ee3aceef9e19bdce21f9 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Wed, 18 Mar 2026 18:19:16 +0100 Subject: [PATCH 809/821] Remove tests for global setting OPTIMIZE_PAGINATION_FOR_SPEED --- tests/methods/get.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/tests/methods/get.py b/tests/methods/get.py index 4fb3f42a5..b3ff26e90 100644 --- a/tests/methods/get.py +++ b/tests/methods/get.py @@ -93,20 +93,6 @@ def test_get_page(self): self.assertPage(response, status) def test_get_perform_count_on_pagination_disabled(self): - self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"] = True - - r = self.test_client.get("%s?page=2" % self.known_resource_url) - self.assert200(r.status_code) - - body = json.loads(r.get_data()) - links = body["_links"] - self.assertFalse("last" in links) - self.assertFalse("total" in body["_meta"]) - self.assertNextLink(links, 3) - self.assertPrevLink(links, 1) - self.assertFalse(self.app.config["HEADER_TOTAL_COUNT"] in r.headers) - - def test_get_perform_count_on_pagination_disabled_on_resource(self): self.app.config["DOMAIN"][self.known_resource]["optimize_pagination_for_speed"] = True r = self.test_client.get("%s?page=2" % self.known_resource_url) @@ -2212,30 +2198,6 @@ def test_getitem_with_custom_idfield(self): self.assertItemResponse(response, status, "products") def test_getitem_optimize_pagination_next_link(self): - """When OPTIMIZE_PAGINATION_FOR_SPEED is enabled, the ``next`` link on - an item endpoint should use the collection URL, not the document URL. - Regression test for a bug where the next href incorrectly included the - document ID (e.g. ``resource/?page=2`` instead of - ``resource?page=2``). - """ - self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"] = True - - response, status = self.get(self.known_resource, item=self.item_id) - self.assert200(status) - - links = response["_links"] - self.assertIn("next", links) - - next_href = links["next"]["href"] - resource_url = self.domain[self.known_resource]["url"] - self.assertTrue( - next_href.startswith("%s?" % resource_url), - "Expected next href to start with '%s?' but got '%s'" - % (resource_url, next_href), - ) - self.assertNotIn(str(self.item_id), next_href) - - def test_getitem_optimize_pagination_next_link_on_resource(self): """When optimize_pagination_for_speed is enabled, the ``next`` link on an item endpoint should use the collection URL, not the document URL. Regression test for a bug where the next href incorrectly included the From 6bd02fc125ba7bc119d17c306867c9b2095729c1 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Wed, 18 Mar 2026 18:28:20 +0100 Subject: [PATCH 810/821] Test OPTIMIZE_PAGINATION_FOR_SPEED in default settings --- tests/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/config.py b/tests/config.py index 612822065..685e901a9 100644 --- a/tests/config.py +++ b/tests/config.py @@ -94,6 +94,7 @@ def test_default_settings(self): self.app.config["JSON_REQUEST_CONTENT_TYPES"], ["application/json"] ) self.assertEqual(self.app.config["NORMALIZE_DOTTED_FIELDS"], True) + self.assertEqual(self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED", False) def test_settings_as_dict(self): my_settings = {"API_VERSION": "override!", "DOMAIN": {"contacts": {}}} @@ -282,6 +283,10 @@ def _test_defaults_for_resource(self, resource): self.assertNotEqual(settings["schema"], None) self.assertEqual(type(settings["schema"]), dict) self.assertEqual(settings["etag_ignore_fields"], None) + self.assertEqual( + settings["optimize_pagination_for_speed"], + self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"]) + ) def test_datasource(self): self._test_datasource_for_resource("invoices") From eee369c86cce863207fab71bc9d3fdc588978516 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Thu, 19 Mar 2026 06:25:19 +0100 Subject: [PATCH 811/821] Fix typo --- tests/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config.py b/tests/config.py index 685e901a9..c6bb2588c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -94,7 +94,7 @@ def test_default_settings(self): self.app.config["JSON_REQUEST_CONTENT_TYPES"], ["application/json"] ) self.assertEqual(self.app.config["NORMALIZE_DOTTED_FIELDS"], True) - self.assertEqual(self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED", False) + self.assertEqual(self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"], False) def test_settings_as_dict(self): my_settings = {"API_VERSION": "override!", "DOMAIN": {"contacts": {}}} From 4fdbb2ad8166d5d7daa786d3a4d466cb7dee3626 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Thu, 19 Mar 2026 06:44:21 +0100 Subject: [PATCH 812/821] Fix typo --- tests/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config.py b/tests/config.py index c6bb2588c..6febe010c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -285,7 +285,7 @@ def _test_defaults_for_resource(self, resource): self.assertEqual(settings["etag_ignore_fields"], None) self.assertEqual( settings["optimize_pagination_for_speed"], - self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"]) + self.app.config["OPTIMIZE_PAGINATION_FOR_SPEED"] ) def test_datasource(self): From bdc6d3d7a9a1bc10b50469e955a0562edf01f252 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Thu, 19 Mar 2026 09:39:42 +0100 Subject: [PATCH 813/821] Add optimize_pagination_for_speed to documentation --- docs/config.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 02039b39d..bbc59c2a9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1150,6 +1150,21 @@ always lowercase. schema. If ``False``, the field which is not included in the patch body will be kept untouched. Defaults to ``True``. +``optimize_pagination_for_speed`` Set this to ``True`` to improve pagination + performance. When optimization is active no + count operation, which can be slow on large + collections, is performed on the database. + This does have a few consequences. + Firstly, no document count is returned. + Secondly, ``HATEOAS`` is less accurate: no + last page link is available, and next page + link is always included, even on last page. + On big collections, switching this feature + on can greatly improve performance. + Defaults to ``False`` (slower performance; + document count included; accurate + ``HATEOAS``). + =============================== =============================================== From 3059643bf0c37448151acff3a39b5c314eceef65 Mon Sep 17 00:00:00 2001 From: Emanuele Di Giacomo Date: Thu, 19 Mar 2026 09:48:31 +0100 Subject: [PATCH 814/821] Shift by a few chars the description of the resource options in config.rst --- docs/config.rst | 644 ++++++++++++++++++++++++------------------------ 1 file changed, 322 insertions(+), 322 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index bbc59c2a9..20ee023e9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -826,330 +826,330 @@ always lowercase. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| -=============================== =============================================== -``url`` The endpoint URL. If omitted the resource key - of the ``DOMAIN`` dict will be used to build - the URL. As an example, ``contacts`` would make - the `people` resource available at - ``/contacts`` (instead of ``/people``). URL can - be as complex as needed and can be nested - relative to another API endpoint (you can have - a ``/contacts`` endpoint and then - a ``/contacts/overseas`` endpoint. Both are - independent of each other and freely - configurable). - - You can also use regexes to setup - subresource-like endpoints. See - :ref:`subresources`. - -``allowed_filters`` List of fields on which filtering is allowed. - Entries in this list work in a hierarchical - way. This means that, for instance, filtering - on ``'dict.sub_dict.foo'`` is allowed if - ``allowed_filters`` contains any of - ``'dict.sub_dict.foo``, ``'dict.sub_dict'`` - or ``'dict'``. Instead filtering on - ``'dict'`` is allowed if ``allowed_filters`` - contains ``'dict'``. - Can be set to ``[]`` (no filters allowed), or - ``['*']`` (fields allowed on every field). - Defaults to ``['*']``. - - *Please note:* If API scraping or DB DoS - attacks are a concern, then globally disabling - filters (see ``ALLOWED_FILTERS`` above) and - then whitelisting valid ones at the local level - is the way to go. - -``sorting`` ``True`` if sorting is enabled, ``False`` - otherwise. Locally overrides ``SORTING``. - -``pagination`` ``True`` if pagination is enabled, ``False`` - otherwise. Locally overrides ``PAGINATION``. - -``pagination_limit`` Maximum value allowed for ``QUERY_MAX_RESULTS`` - query parameter. Values exceeding the - limit will be silently replaced with this - value. You want to aim for a reasonable - compromise between performance and transfer - size. Defaults to 50. - -``resource_methods`` A list of HTTP methods supported at resource - endpoint. Allowed values: ``GET``, ``POST``, - ``DELETE``. Locally overrides - ``RESOURCE_METHODS``. - - *Please note:* if you're running version 0.0.5 - or earlier use the now unsupported ``methods`` - keyword instead. - -``public_methods`` A list of HTTP methods supported at resource - endpoint, open to public access even when - :ref:`auth` is enabled. Locally overrides - ``PUBLIC_METHODS``. - -``item_methods`` A list of HTTP methods supported at item - endpoint. Allowed values: ``GET``, ``PATCH``, - ``PUT`` and ``DELETE``. ``PATCH`` or, for - clients not supporting PATCH, ``POST`` with - the ``X-HTTP-Method-Override`` header tag. - Locally overrides ``ITEM_METHODS``. - -``public_item_methods`` A list of HTTP methods supported at item - endpoint, left open to public access when - :ref:`auth` is enabled. Locally overrides - ``PUBLIC_ITEM_METHODS``. - -``allowed_roles`` A list of allowed `roles` for resource - endpoint. See :ref:`auth` for more - information. Locally overrides - ``ALLOWED_ROLES``. - -``allowed_read_roles`` A list of allowed `roles` for resource - endpoint with GET and OPTIONS methods. - See :ref:`auth` for more - information. Locally overrides - ``ALLOWED_READ_ROLES``. - -``allowed_write_roles`` A list of allowed `roles` for resource - endpoint with POST, PUT and DELETE. - See :ref:`auth` for more - information. Locally overrides - ``ALLOWED_WRITE_ROLES``. - -``allowed_item_read_roles`` A list of allowed `roles` for item endpoint - with GET and OPTIONS methods. - See :ref:`auth` for more information. - Locally overrides ``ALLOWED_ITEM_READ_ROLES``. - - -``allowed_item_write_roles`` A list of allowed `roles` for item endpoint - with PUT, PATH and DELETE methods. - See :ref:`auth` for more information. - Locally overrides ``ALLOWED_ITEM_WRITE_ROLES``. - -``allowed_item_roles`` A list of allowed `roles` for item endpoint. - See :ref:`auth` for more information. - Locally overrides ``ALLOWED_ITEM_ROLES``. - -``cache_control`` Value of the ``Cache-Control`` header field - used when serving ``GET`` requests. Leave empty - if you don't want to include cache directives - with API responses. Locally overrides - ``CACHE_CONTROL``. - -``cache_expires`` Value (in seconds) of the ``Expires`` header - field used when serving ``GET`` requests. If - set to a non-zero value, the header will - always be included, regardless of the setting - of ``CACHE_CONTROL``. Locally overrides - ``CACHE_EXPIRES``. - -``id_field`` Field used to uniquely identify resource items - within the database. Locally overrides - ``ID_FIELD``. - -``item_lookup`` ``True`` if item endpoint should be available, - ``False`` otherwise. Locally overrides - ``ITEM_LOOKUP``. - -``item_lookup_field`` Field used when looking up a resource - item. Locally overrides ``ITEM_LOOKUP_FIELD``. - -``item_url`` Rule used to construct item endpoint URL. - Locally overrides ``ITEM_URL``. - -``resource_title`` Title used when building resource links - (HATEOAS). Defaults to resource's ``url``. - -``item_title`` Title to be used when building item references, - both in XML and JSON responses. Overrides - ``ITEM_TITLE``. - -``additional_lookup`` Besides the standard item endpoint which - defaults to ``//``, - you can optionally define a secondary, - read-only, endpoint like - ``//``. You do so by - defining a dictionary comprised of two items - `field` and `url`. The former is the name of - the field used for the lookup. If the field - type (as defined in the resource schema_) is - a string, then you put a URL rule in `url`. If - it is an integer, then you just omit `url`, as - it is automatically handled. See the code - snippet below for an usage example of this - feature. - -``datasource`` Explicitly links API resources to database - collections. See `Advanced Datasource - Patterns`_. - -``auth_field`` Enables :ref:`user-restricted`. When the - feature is enabled, users can only - read/update/delete resource items created by - themselves. The keyword contains the actual - name of the field used to store the id of - the user who created the resource item. Locally - overrides ``AUTH_FIELD``. - -``allow_unknown`` When ``True``, this option will allow insertion - of arbitrary, unknown fields to the endpoint. - Use with caution. Locally overrides - ``ALLOW_UNKNOWN``. See :ref:`unknown` for more - information. Defaults to ``False``. - -``projection`` When ``True``, this option enables the - :ref:`projections` feature. Locally overrides - ``PROJECTION``. Defaults to ``True``. - -``embedding`` When ``True`` this option enables the - :ref:`embedded_docs` feature. Defaults to - ``True``. - -``extra_response_fields`` Allows to configure a list of additional - document fields that should be provided with - every POST response. Normally only - automatically handled fields (``ID_FIELD``, - ``LAST_UPDATED``, ``DATE_CREATED``, ``ETAG``) - are included in response payloads. Overrides - ``EXTRA_RESPONSE_FIELDS``. - -``hateoas`` When ``False``, this option disables - :ref:`hateoas_feature` for the resource. - Defaults to ``True``. +=================================== =============================================== +``url`` The endpoint URL. If omitted the resource key + of the ``DOMAIN`` dict will be used to build + the URL. As an example, ``contacts`` would make + the `people` resource available at + ``/contacts`` (instead of ``/people``). URL can + be as complex as needed and can be nested + relative to another API endpoint (you can have + a ``/contacts`` endpoint and then + a ``/contacts/overseas`` endpoint. Both are + independent of each other and freely + configurable). + + You can also use regexes to setup + subresource-like endpoints. See + :ref:`subresources`. + +``allowed_filters`` List of fields on which filtering is allowed. + Entries in this list work in a hierarchical + way. This means that, for instance, filtering + on ``'dict.sub_dict.foo'`` is allowed if + ``allowed_filters`` contains any of + ``'dict.sub_dict.foo``, ``'dict.sub_dict'`` + or ``'dict'``. Instead filtering on + ``'dict'`` is allowed if ``allowed_filters`` + contains ``'dict'``. + Can be set to ``[]`` (no filters allowed), or + ``['*']`` (fields allowed on every field). + Defaults to ``['*']``. -``mongo_query_whitelist`` A list of extra Mongo query operators to allow - for this endpoint besides the official list of - allowed operators. Defaults to ``[]``. - -``mongo_write_concern`` A dictionary defining MongoDB write concern - settings for the endpoint datasource. All - standard write concern settings (w, wtimeout, j, - fsync) are supported. Defaults to ``{'w': 1}`` - which means 'do regular acknowledged writes' - (this is also the Mongo default.) - - Please be aware that setting 'w' to a value of - 2 or greater requires replication to be active - or you will be getting 500 errors (the write - will still happen; Mongo will just be unable - to check that it's being written to multiple - servers.) - -``mongo_prefix`` Allows overriding of the default ``MONGO`` - prefix, which is used when retrieving MongoDB - settings from configuration. - - For example if ``mongo_prefix`` is set to - ``MONGO2`` then, when serving requests for the - endpoint, ``MONGO2`` prefixed settings will - be used to access the database. - - This allows for eventually serving data from - a different database/server at every endpoint. - - See also: :ref:`authdrivendb`. - -``mongo_indexes`` Allows to specify a set of indexes to be - created for this resource before the app is - launched. - - Indexes are expressed as a dict where keys are - index names and values are either a list of - tuples of (field, direction) pairs, or - a tuple with a list of field/direction pairs - *and* index options expressed as a dict, such - as ``{'index name': [('field', 1)], 'index with - args': ([('field', 1)], {"sparse": True})}``. - - Multiple pairs are used to create compound - indexes. Direction takes all kind of values - supported by PyMongo, such as ``ASCENDING`` - = 1 and ``DESCENDING`` = -1. All index options - such as ``sparse``, ``min``, ``max``, - etc. are supported (see PyMongo_ documentation.) - - *Please note:* keep in mind that index design, - creation and maintenance is a very important - task and should be planned and executed with - great care. Usually it is also a very resource - intensive operation. You might therefore want - to handle this task manually, out of the - context of API instantiation. Also remember - that, by default, any already existent index - for which the definition has been changed, will - be dropped and re-created. - -``authentication`` A class with the authorization logic for the - endpoint. If not provided the eventual - general purpose auth class (passed as - application constructor argument) will be used. - For details on authentication and authorization - see :ref:`auth`. Defaults to ``None``, - -``embedded_fields`` A list of fields for which :ref:`embedded_docs` - is enabled by default. For this feature to work - properly fields in the list must be - ``embeddable``, and ``embedding`` must be - active for the resource. - -``query_objectid_as_string`` When enabled the Mongo parser will avoid - automatically casting electable strings to - ObjectIds. This can be useful in those rare - occurrences where you have string fields in the - database whose values can actually be casted to - ObjectId values, but shouldn't. It effects - queries (``?where=``) and parsing of payloads. - Defaults to ``False``. + *Please note:* If API scraping or DB DoS + attacks are a concern, then globally disabling + filters (see ``ALLOWED_FILTERS`` above) and + then whitelisting valid ones at the local level + is the way to go. + +``sorting`` ``True`` if sorting is enabled, ``False`` + otherwise. Locally overrides ``SORTING``. + +``pagination`` ``True`` if pagination is enabled, ``False`` + otherwise. Locally overrides ``PAGINATION``. + +``pagination_limit`` Maximum value allowed for ``QUERY_MAX_RESULTS`` + query parameter. Values exceeding the + limit will be silently replaced with this + value. You want to aim for a reasonable + compromise between performance and transfer + size. Defaults to 50. + +``resource_methods`` A list of HTTP methods supported at resource + endpoint. Allowed values: ``GET``, ``POST``, + ``DELETE``. Locally overrides + ``RESOURCE_METHODS``. + + *Please note:* if you're running version 0.0.5 + or earlier use the now unsupported ``methods`` + keyword instead. + +``public_methods`` A list of HTTP methods supported at resource + endpoint, open to public access even when + :ref:`auth` is enabled. Locally overrides + ``PUBLIC_METHODS``. + +``item_methods`` A list of HTTP methods supported at item + endpoint. Allowed values: ``GET``, ``PATCH``, + ``PUT`` and ``DELETE``. ``PATCH`` or, for + clients not supporting PATCH, ``POST`` with + the ``X-HTTP-Method-Override`` header tag. + Locally overrides ``ITEM_METHODS``. + +``public_item_methods`` A list of HTTP methods supported at item + endpoint, left open to public access when + :ref:`auth` is enabled. Locally overrides + ``PUBLIC_ITEM_METHODS``. + +``allowed_roles`` A list of allowed `roles` for resource + endpoint. See :ref:`auth` for more + information. Locally overrides + ``ALLOWED_ROLES``. + +``allowed_read_roles`` A list of allowed `roles` for resource + endpoint with GET and OPTIONS methods. + See :ref:`auth` for more + information. Locally overrides + ``ALLOWED_READ_ROLES``. + +``allowed_write_roles`` A list of allowed `roles` for resource + endpoint with POST, PUT and DELETE. + See :ref:`auth` for more + information. Locally overrides + ``ALLOWED_WRITE_ROLES``. + +``allowed_item_read_roles`` A list of allowed `roles` for item endpoint + with GET and OPTIONS methods. + See :ref:`auth` for more information. + Locally overrides ``ALLOWED_ITEM_READ_ROLES``. + + +``allowed_item_write_roles`` A list of allowed `roles` for item endpoint + with PUT, PATH and DELETE methods. + See :ref:`auth` for more information. + Locally overrides ``ALLOWED_ITEM_WRITE_ROLES``. + +``allowed_item_roles`` A list of allowed `roles` for item endpoint. + See :ref:`auth` for more information. + Locally overrides ``ALLOWED_ITEM_ROLES``. + +``cache_control`` Value of the ``Cache-Control`` header field + used when serving ``GET`` requests. Leave empty + if you don't want to include cache directives + with API responses. Locally overrides + ``CACHE_CONTROL``. + +``cache_expires`` Value (in seconds) of the ``Expires`` header + field used when serving ``GET`` requests. If + set to a non-zero value, the header will + always be included, regardless of the setting + of ``CACHE_CONTROL``. Locally overrides + ``CACHE_EXPIRES``. + +``id_field`` Field used to uniquely identify resource items + within the database. Locally overrides + ``ID_FIELD``. + +``item_lookup`` ``True`` if item endpoint should be available, + ``False`` otherwise. Locally overrides + ``ITEM_LOOKUP``. + +``item_lookup_field`` Field used when looking up a resource + item. Locally overrides ``ITEM_LOOKUP_FIELD``. + +``item_url`` Rule used to construct item endpoint URL. + Locally overrides ``ITEM_URL``. + +``resource_title`` Title used when building resource links + (HATEOAS). Defaults to resource's ``url``. + +``item_title`` Title to be used when building item references, + both in XML and JSON responses. Overrides + ``ITEM_TITLE``. + +``additional_lookup`` Besides the standard item endpoint which + defaults to ``//``, + you can optionally define a secondary, + read-only, endpoint like + ``//``. You do so by + defining a dictionary comprised of two items + `field` and `url`. The former is the name of + the field used for the lookup. If the field + type (as defined in the resource schema_) is + a string, then you put a URL rule in `url`. If + it is an integer, then you just omit `url`, as + it is automatically handled. See the code + snippet below for an usage example of this + feature. + +``datasource`` Explicitly links API resources to database + collections. See `Advanced Datasource + Patterns`_. + +``auth_field`` Enables :ref:`user-restricted`. When the + feature is enabled, users can only + read/update/delete resource items created by + themselves. The keyword contains the actual + name of the field used to store the id of + the user who created the resource item. Locally + overrides ``AUTH_FIELD``. + +``allow_unknown`` When ``True``, this option will allow insertion + of arbitrary, unknown fields to the endpoint. + Use with caution. Locally overrides + ``ALLOW_UNKNOWN``. See :ref:`unknown` for more + information. Defaults to ``False``. -``internal_resource`` When ``True``, this option makes the resource - internal. No HTTP action can be performed on - the endpoint, which is still accessible from - the Eve data layer. See - :ref:`internal_resources` for more - information. Defaults to ``False``. - -``etag_ignore_fields`` List of fields that - should not be used to compute the ETag value. - Defaults to ``None`` which means that by - default all fields are included in the computation. - It looks like ``['field1', 'field2', - 'field3.nested_field', ...]``. - -``schema`` A dict defining the actual data structure being - handled by the resource. Enables data - validation. See `Schema Definition`_. - -``bulk_enabled`` When ``True`` this option enables the - :ref:`bulk_insert` feature for this resource. - Locally overrides ``BULK_ENABLED``. - -``soft_delete`` When ``True`` this option enables the - :ref:`soft_delete` feature for this resource. - Locally overrides ``SOFT_DELETE``. - -``merge_nested_documents`` If ``True``, updates to nested fields are - merged with the current data on ``PATCH``. - If ``False``, the updates overwrite the - current data. Locally overrides - ``MERGE_NESTED_DOCUMENTS``. -``normalize_dotted_fields`` If ``True``, dotted fields are parsed and - processed as subdocument fields. If ``False``, - dotted fields are left unparsed and - unprocessed, and the payload is passed to the - underlying data-layer as-is. Please note that - with the default Mongo layer, setting this to - ``False`` will result in an error. Defaults to - ``True``. -``normalize_on_patch`` If ``True``, the patch document will be - normalized according to schema. This means if - a field is not included in the patch body, it - will be reset to the default value in its - schema. If ``False``, the field which is not - included in the patch body will be kept - untouched. Defaults to ``True``. +``projection`` When ``True``, this option enables the + :ref:`projections` feature. Locally overrides + ``PROJECTION``. Defaults to ``True``. + +``embedding`` When ``True`` this option enables the + :ref:`embedded_docs` feature. Defaults to + ``True``. + +``extra_response_fields`` Allows to configure a list of additional + document fields that should be provided with + every POST response. Normally only + automatically handled fields (``ID_FIELD``, + ``LAST_UPDATED``, ``DATE_CREATED``, ``ETAG``) + are included in response payloads. Overrides + ``EXTRA_RESPONSE_FIELDS``. + +``hateoas`` When ``False``, this option disables + :ref:`hateoas_feature` for the resource. + Defaults to ``True``. + +``mongo_query_whitelist`` A list of extra Mongo query operators to allow + for this endpoint besides the official list of + allowed operators. Defaults to ``[]``. + +``mongo_write_concern`` A dictionary defining MongoDB write concern + settings for the endpoint datasource. All + standard write concern settings (w, wtimeout, j, + fsync) are supported. Defaults to ``{'w': 1}`` + which means 'do regular acknowledged writes' + (this is also the Mongo default.) + + Please be aware that setting 'w' to a value of + 2 or greater requires replication to be active + or you will be getting 500 errors (the write + will still happen; Mongo will just be unable + to check that it's being written to multiple + servers.) + +``mongo_prefix`` Allows overriding of the default ``MONGO`` + prefix, which is used when retrieving MongoDB + settings from configuration. + + For example if ``mongo_prefix`` is set to + ``MONGO2`` then, when serving requests for the + endpoint, ``MONGO2`` prefixed settings will + be used to access the database. + + This allows for eventually serving data from + a different database/server at every endpoint. + + See also: :ref:`authdrivendb`. + +``mongo_indexes`` Allows to specify a set of indexes to be + created for this resource before the app is + launched. + + Indexes are expressed as a dict where keys are + index names and values are either a list of + tuples of (field, direction) pairs, or + a tuple with a list of field/direction pairs + *and* index options expressed as a dict, such + as ``{'index name': [('field', 1)], 'index with + args': ([('field', 1)], {"sparse": True})}``. + + Multiple pairs are used to create compound + indexes. Direction takes all kind of values + supported by PyMongo, such as ``ASCENDING`` + = 1 and ``DESCENDING`` = -1. All index options + such as ``sparse``, ``min``, ``max``, + etc. are supported (see PyMongo_ documentation.) + + *Please note:* keep in mind that index design, + creation and maintenance is a very important + task and should be planned and executed with + great care. Usually it is also a very resource + intensive operation. You might therefore want + to handle this task manually, out of the + context of API instantiation. Also remember + that, by default, any already existent index + for which the definition has been changed, will + be dropped and re-created. + +``authentication`` A class with the authorization logic for the + endpoint. If not provided the eventual + general purpose auth class (passed as + application constructor argument) will be used. + For details on authentication and authorization + see :ref:`auth`. Defaults to ``None``, + +``embedded_fields`` A list of fields for which :ref:`embedded_docs` + is enabled by default. For this feature to work + properly fields in the list must be + ``embeddable``, and ``embedding`` must be + active for the resource. + +``query_objectid_as_string`` When enabled the Mongo parser will avoid + automatically casting electable strings to + ObjectIds. This can be useful in those rare + occurrences where you have string fields in the + database whose values can actually be casted to + ObjectId values, but shouldn't. It effects + queries (``?where=``) and parsing of payloads. + Defaults to ``False``. + +``internal_resource`` When ``True``, this option makes the resource + internal. No HTTP action can be performed on + the endpoint, which is still accessible from + the Eve data layer. See + :ref:`internal_resources` for more + information. Defaults to ``False``. + +``etag_ignore_fields`` List of fields that + should not be used to compute the ETag value. + Defaults to ``None`` which means that by + default all fields are included in the computation. + It looks like ``['field1', 'field2', + 'field3.nested_field', ...]``. + +``schema`` A dict defining the actual data structure being + handled by the resource. Enables data + validation. See `Schema Definition`_. + +``bulk_enabled`` When ``True`` this option enables the + :ref:`bulk_insert` feature for this resource. + Locally overrides ``BULK_ENABLED``. + +``soft_delete`` When ``True`` this option enables the + :ref:`soft_delete` feature for this resource. + Locally overrides ``SOFT_DELETE``. + +``merge_nested_documents`` If ``True``, updates to nested fields are + merged with the current data on ``PATCH``. + If ``False``, the updates overwrite the + current data. Locally overrides + ``MERGE_NESTED_DOCUMENTS``. +``normalize_dotted_fields`` If ``True``, dotted fields are parsed and + processed as subdocument fields. If ``False``, + dotted fields are left unparsed and + unprocessed, and the payload is passed to the + underlying data-layer as-is. Please note that + with the default Mongo layer, setting this to + ``False`` will result in an error. Defaults to + ``True``. +``normalize_on_patch`` If ``True``, the patch document will be + normalized according to schema. This means if + a field is not included in the patch body, it + will be reset to the default value in its + schema. If ``False``, the field which is not + included in the patch body will be kept + untouched. Defaults to ``True``. ``optimize_pagination_for_speed`` Set this to ``True`` to improve pagination performance. When optimization is active no count operation, which can be slow on large From 3091d8e585dfbf70c085068481e53c9ab5d81e4f Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 19 Mar 2026 09:59:47 +0100 Subject: [PATCH 815/821] Emanuele di Giacomo --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b7268335b..b81522b56 100644 --- a/AUTHORS +++ b/AUTHORS @@ -58,6 +58,7 @@ Patches and Contributions - Dougal Matthews - Einar Huseby - Elias García +- Emanuele Di Giacomo - Emmanuel Leblond - Eugene Prikazchikov - Ewan Higgs From 1936110c30e45fb962f2cb33bf00b810670eb83a Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 19 Mar 2026 10:04:18 +0100 Subject: [PATCH 816/821] changelog for #1569 --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 114043114..baec1a487 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development -------------- -- *hic sunt leones* +- new: ``optimize_pagination_for_speed``, a resource-level setting that allows granular control overriding the global configuration (`#1569`_) + +.. _`#1569`: https://github.com/pyeve/eve/pull/1567 Version v2.2.5 -------------- From 8757f9ba9ceab50bd18e26a856d443ea36f6d5e7 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 19 Mar 2026 10:06:18 +0100 Subject: [PATCH 817/821] bump version to 2.3.0 --- CHANGES.rst | 7 +++++++ eve/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index baec1a487..10dea1e2a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ Here you can see the full list of changes between each Eve release. In Development -------------- +- *hic sunt leones* + +Version v2.3.0 +-------------- + +Released on March 19, 2026. + - new: ``optimize_pagination_for_speed``, a resource-level setting that allows granular control overriding the global configuration (`#1569`_) .. _`#1569`: https://github.com/pyeve/eve/pull/1567 diff --git a/eve/__init__.py b/eve/__init__.py index ef22bde38..b11988cd2 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.2.5" +__version__ = "2.3.0" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From 217b74503cf1453c745cdaae4ccd994354cc1837 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Thu, 19 Mar 2026 10:44:45 +0100 Subject: [PATCH 818/821] fix: changelog link --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 10dea1e2a..7afc3b5b6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,7 +15,7 @@ Released on March 19, 2026. - new: ``optimize_pagination_for_speed``, a resource-level setting that allows granular control overriding the global configuration (`#1569`_) -.. _`#1569`: https://github.com/pyeve/eve/pull/1567 +.. _`#1569`: https://github.com/pyeve/eve/pull/1569 Version v2.2.5 -------------- From ebb037040ba7341a472b5417bbb32a7c236289dd Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 20 Mar 2026 11:17:19 +0100 Subject: [PATCH 819/821] docs: add security warnings about query operator field enumeration Strengthen documentation for ALLOWED_FILTERS, MONGO_QUERY_BLACKLIST, and the Filtering section with explicit warnings about the risk of blind value enumeration via comparison operators on sensitive fields. --- CHANGES.rst | 4 +++- docs/config.rst | 29 +++++++++++++++++++++++++---- docs/features.rst | 14 +++++++++++++- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7afc3b5b6..b3ff2773d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Here you can see the full list of changes between each Eve release. In Development -------------- -- *hic sunt leones* +- Docs: added security warnings about blind field enumeration via query + operators to ``ALLOWED_FILTERS``, ``MONGO_QUERY_BLACKLIST``, and the + Filtering section. Version v2.3.0 -------------- diff --git a/docs/config.rst b/docs/config.rst index 20ee023e9..ab229eefe 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -130,10 +130,23 @@ uppercase. local level (see ``allowed_filters`` below). Defaults to ``['*']``. - *Please note:* If API scraping or DB DoS - attacks are a concern, then globally - disabling filters and whitelisting valid - ones at the local level is the way to go. + .. warning:: + + **Security:** With the default setting + (``['*']``), clients can filter on + *any* field, including sensitive ones + such as password hashes or tokens. + MongoDB query operators like ``$gt``, + ``$lt``, and ``$ne`` can be used by + attackers to perform blind enumeration + of field values. For production + deployments, globally disable filters + (set to ``[]``) and explicitly + whitelist only the fields you intend + to be queryable at the resource level + via ``allowed_filters``. Never allow + filtering on fields that store secrets + or credentials. ``VALIDATE_FILTERS`` Whether to validate the filters against the resource schema. Invalid filters will throw @@ -579,6 +592,14 @@ uppercase. easily replaced with the (very rich) Mongo query dialect. + .. warning:: + + Removing ``$where`` or ``$regex`` from + this list exposes your application to + server-side JavaScript injection and + ReDoS attacks. Only do so if you fully + understand the implications. + ``MONGO_QUERY_WHITELIST`` A list of extra Mongo query operators to allow besides the official list of allowed operators. Defaults to ``[]``. diff --git a/docs/features.rst b/docs/features.rst index 753954719..1eac36c98 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -330,7 +330,19 @@ filters is the way to go. You also have the option to validate the incoming filters against the resource's schema and refuse to apply the filtering if any filters are invalid, by using the -``VALIDATE_FILTERING`` system setting (see :ref:`global`) +``VALIDATE_FILTERING`` system setting (see :ref:`global`). + +.. warning:: + + **Security:** Since Eve exposes MongoDB's query operators to API consumers, + care must be taken when deciding which fields are filterable. With the default + configuration (``ALLOWED_FILTERS = ['*']``), comparison operators such as + ``$gt``, ``$lt``, and ``$ne`` can be used to blindly enumerate values of any + field, including sensitive ones like password hashes or tokens. For production + APIs, you should restrict ``ALLOWED_FILTERS`` to only the fields that are + intended to be queryable, and never allow filtering on fields that contain + secrets or credentials. Eve also blacklists dangerous operators like ``$where`` + and ``$regex`` by default via ``MONGO_QUERY_BLACKLIST`` (see :ref:`global`). Pretty Printing --------------- From 75c6373b89cc28e89b9806f9dfce58e6a44dd55b Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Fri, 20 Mar 2026 11:24:47 +0100 Subject: [PATCH 820/821] fix: validate JSONP callback and deprecate JSONP support Validate the JSONP callback parameter against a strict identifier pattern to prevent XSS injection. Deprecate JSONP_ARGUMENT in favor of CORS, with a DeprecationWarning at startup and updated docs. --- CHANGES.rst | 2 ++ docs/config.rst | 10 ++++++++-- docs/features.rst | 5 +++++ eve/flaskapp.py | 8 ++++++++ eve/render.py | 4 +++- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b3ff2773d..82b9506c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ In Development - Docs: added security warnings about blind field enumeration via query operators to ``ALLOWED_FILTERS``, ``MONGO_QUERY_BLACKLIST``, and the Filtering section. +- Fix: validate JSONP callback parameter to prevent XSS injection. +- Deprecation: ``JSONP_ARGUMENT`` is deprecated. Use CORS instead. Version v2.3.0 -------------- diff --git a/docs/config.rst b/docs/config.rst index ab229eefe..a655902c9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -736,13 +736,19 @@ uppercase. loading posts themselves. Defaults to ``X-Total-Count``. -``JSONP_ARGUMENT`` This option will cause the response to be +``JSONP_ARGUMENT`` .. deprecated:: + JSONP is deprecated and will be removed + in a future release. Use CORS instead. + + This option will cause the response to be wrapped in a JavaScript function call if the argument is set in the request. For example if you set ``JSON_ARGUMENT = 'callback'``, then all responses to ``?callback=funcname`` requests will be - wrapped in a ``funcname`` call. Defaults to + wrapped in a ``funcname`` call. The + callback name is validated to be a safe + JavaScript identifier. Defaults to ``None``. ``BULK_ENABLED`` Enables bulk insert when set to ``True``. diff --git a/docs/features.rst b/docs/features.rst index 1eac36c98..deee78a32 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -932,6 +932,11 @@ anchor and escape the regexes properly, for example JSONP Support ------------- +.. deprecated:: + JSONP support is deprecated and will be removed in a future release. + Use CORS (Cross-Origin Resource Sharing) instead, which is supported by + all modern browsers and does not carry the security risks inherent to JSONP. + In general you don't really want to add JSONP when you can enable CORS instead: There have been some criticisms raised about JSONP. Cross-origin resource diff --git a/eve/flaskapp.py b/eve/flaskapp.py index 36899cd66..73641e9ec 100644 --- a/eve/flaskapp.py +++ b/eve/flaskapp.py @@ -308,6 +308,14 @@ def deprecated_renderers_settings(): deprecated_renderers_settings() + if self.config.get("JSONP_ARGUMENT"): + warnings.warn( + "JSONP_ARGUMENT is deprecated and will be removed in a future " + "release. Use CORS (Cross-Origin Resource Sharing) instead.", + DeprecationWarning, + stacklevel=2, + ) + def validate_domain_struct(self): """Validates that Eve configuration settings conform to the requirements. diff --git a/eve/render.py b/eve/render.py index da7bf2e1d..5a060157a 100644 --- a/eve/render.py +++ b/eve/render.py @@ -147,11 +147,13 @@ def _prepare_response( # invoke the render function and obtain the corresponding rendered item rendered = renderer_cls().render(dct) - # JSONP + # JSONP (deprecated) if config.JSONP_ARGUMENT: jsonp_arg = config.JSONP_ARGUMENT if jsonp_arg in request.args and "json" in mime: callback = request.args.get(jsonp_arg) + if not re.match(r"^[a-zA-Z_$][\w$.]*$", callback): + abort(400, description="Invalid JSONP callback name") rendered = "%s(%s)" % (callback, rendered) # build the main wsgi response object From fe7d9c919bf35fe149feb42e51ba9c5c337e3119 Mon Sep 17 00:00:00 2001 From: Nicola Iarocci Date: Tue, 24 Mar 2026 09:01:04 +0100 Subject: [PATCH 821/821] bump version to 2.3.1 --- CHANGES.rst | 10 ++++- eve/__init__.py | 2 +- response.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 response.md diff --git a/CHANGES.rst b/CHANGES.rst index 82b9506c9..924958f11 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,15 @@ Here you can see the full list of changes between each Eve release. In Development -------------- +- *hic sunt leones* + +Version v2.3.1 +-------------- + +Released on March 24, 2026. + - Docs: added security warnings about blind field enumeration via query - operators to ``ALLOWED_FILTERS``, ``MONGO_QUERY_BLACKLIST``, and the - Filtering section. + operators to ``ALLOWED_FILTERS``, ``MONGO_QUERY_BLACKLIST``, and the Filtering section. - Fix: validate JSONP callback parameter to prevent XSS injection. - Deprecation: ``JSONP_ARGUMENT`` is deprecated. Use CORS instead. diff --git a/eve/__init__.py b/eve/__init__.py index b11988cd2..d963757fa 100644 --- a/eve/__init__.py +++ b/eve/__init__.py @@ -38,7 +38,7 @@ """ -__version__ = "2.3.0" +__version__ = "2.3.1" # RFC 1123 (ex RFC 822) DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/response.md b/response.md new file mode 100644 index 000000000..aabae5b09 --- /dev/null +++ b/response.md @@ -0,0 +1,111 @@ +# Security Report Review — Eve Framework + +Thank you for taking the time to analyze the Eve codebase and submit these six security reports. We take security seriously and have carefully reviewed each finding against the current codebase. + +After thorough analysis, we found that **one report (#4) identified a genuine code defect**, which we have already fixed. The remaining five reports describe attack vectors that are either already mitigated by Eve's default configuration or based on an incorrect understanding of the code flow. + +Below is our detailed response to each report, followed by a summary. + +--- + +## Report #1 — MongoDB Query Injection via Unsanitized 'where' Parameter (CVSS 8.7) + +**Assessment: Mostly mitigated; documentation improved.** + +Two of the three PoC examples are already mitigated by Eve's default configuration: + +- `{"$where": "sleep(5000)"}` — **blocked**. `$where` is in `MONGO_QUERY_BLACKLIST` by default since Eve 0.7.x (and the recursive traversal bypass was fixed in 0.7.10). +- `{"email": {"$regex": ".*"}}` — **blocked**. `$regex` is also in the default blacklist. + +The third PoC (`{"password": {"$gt": ""}}`) does work with default settings, as `$gt` is a legitimate MongoDB comparison operator required for normal API operation (e.g., date range queries). + +**Regarding `auth_field` bypass:** the report states that injected conditions can bypass `auth_field`-protected resources. This is inaccurate — the `auth_field` filter is applied server-side as an additional AND condition on the query. A client-supplied `where` clause cannot override or remove it; documents belonging to other users are not returned. + +**Regarding the actual risk:** with the default configuration (`ALLOWED_FILTERS = ['*']`), comparison operators like `$gt`/`$lt`/`$ne` can be used for blind enumeration of field values on any filterable field. This is a known trade-off of Eve's design as a flexible REST framework — it intentionally exposes MongoDB's query language to API consumers. Eve provides the configuration tools to mitigate this (`ALLOWED_FILTERS`, `VALIDATE_FILTERS`, `MONGO_QUERY_BLACKLIST`), and we have now strengthened the documentation with explicit security warnings about this. + +We do not consider this a code vulnerability warranting a CVE, as the framework behaves as designed and provides adequate configuration options for hardening. + +--- + +## Report #2 — MongoDB Operator Injection via $where JavaScript Execution (CVSS 9.3) + +**Assessment: Not a vulnerability.** + +While `$where` is listed in the `Mongo.operators` set (the set of *recognized* MongoDB operators), it is also included in `MONGO_QUERY_BLACKLIST`, which defaults to `['$where', '$regex']`. The `_sanitize()` method in `eve/io/mongo/mongo.py` checks incoming queries against this blacklist and aborts with a 400 error if any blacklisted operator is found. This check is applied recursively to nested query structures. + +All PoCs in this report — both the DoS via busy-loop and the blind extraction via boolean/timing channels — are rejected with a 400 response under default settings. The `$where` operator would only be executable if an administrator explicitly removes it from `MONGO_QUERY_BLACKLIST`, which is a deliberate opt-in. + +The CVSS 9.3 rating is not applicable as the attack surface does not exist under default configuration. + +--- + +## Report #3 — MongoDB ReDoS via Uncontrolled $regex Operator (CVSS 8.7) + +**Assessment: Not a vulnerability.** + +This report follows the same pattern as #2. While `$regex` is listed in the `Mongo.operators` set, it is **also included in `MONGO_QUERY_BLACKLIST`**, which defaults to `['$where', '$regex']`. + +All PoCs — including the catastrophic backtracking patterns — are rejected with a 400 response under default settings. The `_sanitize()` method blocks `$regex` before the query ever reaches MongoDB. The operator would only be usable if an administrator explicitly removes it from the blacklist. + +The CVSS 8.7 rating is not applicable as the attack surface does not exist under default configuration. + +--- + +## Report #4 — JSONP Callback Injection via Unvalidated User Input (CVSS 7.1) + +**Assessment: Valid finding. Fixed.** + +When JSONP support was explicitly enabled via `JSONP_ARGUMENT`, the callback parameter was interpolated into the response without validation, allowing arbitrary JavaScript injection. + +We have addressed this with two changes: + +1. **Fix:** The JSONP callback is now validated against a strict pattern (`^[a-zA-Z_$][\w$.]*$`) ensuring only valid JavaScript identifiers are accepted. Invalid callback names are rejected with a 400 response. + +2. **Deprecation:** `JSONP_ARGUMENT` is now deprecated and will be removed in a future release. JSONP is a legacy technology superseded by CORS, which Eve already supports. A `DeprecationWarning` is emitted at startup when the setting is configured. + +We note that the CVSS 7.1 rating overstates the practical impact: JSONP is disabled by default (`JSONP_ARGUMENT = None`) and requires explicit opt-in. + +--- + +## Report #5 — IDOR via Auth Field Bypass on PUT with Upsert (CVSS 5.9) + +**Assessment: Not a vulnerability.** + +The report claims that during the PUT upsert path, an attacker can supply an arbitrary `auth_field` value in the request body to create documents attributed to another user. This is incorrect. + +When `UPSERT_ON_PUT` triggers `post_internal()`, the function calls `resolve_user_restricted_access()` (in `eve/methods/common.py`), which **unconditionally overwrites** the `auth_field` with the authenticated user's identity: + +```python +document[auth_field] = request_auth_value +``` + +This is not a conditional assignment — any attacker-supplied value is replaced with the real authenticated user's identity before the document is persisted. The PoC would result in a document owned by `user_b` (the actual authenticated user), not `user_a` as claimed. + +--- + +## Report #6 — GridFS Arbitrary File Retrieval via ObjectId Manipulation (CVSS 7.1) + +**Assessment: Not a vulnerability as described.** + +The specific attack path described — accessing files across resources via resource endpoints — is inaccurate. When media fields are embedded in documents (the default behavior, `RETURN_MEDIA_AS_URL = False`), file content is served as part of the document response and goes through the normal document retrieval pipeline, including `auth_field` enforcement and all access control checks. + +We acknowledge a tangential concern: when `RETURN_MEDIA_AS_URL` is set to `True` (not the default), the global `/media/` endpoint serves files from GridFS based solely on the ObjectId, with only generic authentication and no ownership check. This is a known design choice — the ObjectId acts as an opaque capability token, and access control is enforced at the document level. ObjectIds must be known to be exploited and are only revealed through authorized document access. + +The PoC conflates resource endpoints with the media endpoint and does not demonstrate the claimed attack. + +--- + +## Summary + +| # | Report | CVSS | Assessment | Action Taken | +|---|--------|------|------------|--------------| +| 1 | MongoDB Query Injection via `where` | 8.7 | Mostly mitigated by default | Improved documentation | +| 2 | `$where` JavaScript Execution | 9.3 | Blocked by default blacklist | None required | +| 3 | `$regex` ReDoS | 8.7 | Blocked by default blacklist | None required | +| 4 | JSONP Callback Injection | 7.1 | **Valid** (opt-in feature) | Fixed + deprecated JSONP | +| 5 | IDOR via Auth Field on PUT Upsert | 5.9 | Incorrect — auth_field is overwritten | None required | +| 6 | GridFS IDOR via ObjectId | 7.1 | Inaccurate attack path | None required | + +Reports #2 and #3 appear to have been produced by analyzing the `Mongo.operators` set in isolation, without tracing the full query pipeline through the `_sanitize()` method and `MONGO_QUERY_BLACKLIST`. We encourage future analysis to follow the complete code path from request to database execution. + +We appreciate the effort in examining Eve's security posture and welcome further reports that identify actual code defects.