diff --git a/example/app.py b/example/app.py index 34d064f2..c10c3f61 100644 --- a/example/app.py +++ b/example/app.py @@ -40,7 +40,7 @@ class UserResource(Resource): @api.register() class UserView(ResourceView): resource = UserResource - methods = [Create, Update, Fetch, List, Delete] + methods = [Create, Update, Fetch, BulkFetch, Delete] class ContentResource(Resource): document = documents.Content @@ -87,7 +87,7 @@ def update_object(self, obj, data=None, save=True, parent_resources=None): @api.register(name='posts', url='/posts/') class PostView(ResourceView): resource = PostResource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] class LimitedPostResource(Resource): document = documents.Post @@ -98,7 +98,7 @@ class LimitedPostResource(Resource): @api.register(name='limited_posts', url='/limited_posts/') class LimitedPostView(ResourceView): resource = LimitedPostResource - methods = [Create, Update, Fetch, List] + methods = [Create, Update, Fetch, BulkFetch] class DummyAuthenication(AuthenticationBase): def authorized(self): @@ -107,7 +107,7 @@ def authorized(self): @api.register(name='auth', url='/auth/') class DummyAuthView(ResourceView): resource = PostResource - methods = [Create, Update, Fetch, List, Delete] + methods = [Create, Update, Fetch, BulkFetch, Delete] authentication_methods = [DummyAuthenication] @api.register(name='restricted', url='/restricted/') @@ -115,7 +115,7 @@ class RestrictedPostView(ResourceView): """This class allows us to put restrictions in place regarding who/what can be read, changed, added or deleted""" resource = PostResource - methods = [Create, Update, Fetch, List, Delete] + methods = [Create, Update, Fetch, BulkFetch, Delete] # Can't read a post if it isn't published def has_read_permission(self, request, qs): @@ -153,13 +153,13 @@ def upper_name(self, obj): @api.register(name='test', url='/test/') class TestView(ResourceView): resource = TestResource - methods = [Create, Update, Fetch, List] + methods = [Create, Update, Fetch, BulkFetch] @api.register(name='testfields', url='/testfields/') class TestFieldsResource(ResourceView): resource = TestFieldsResource - methods = [Create, Update, Fetch, List] + methods = [Create, Update, Fetch, BulkFetch] class LanguageResource(Resource): document = documents.Language @@ -175,7 +175,7 @@ class PersonResource(Resource): @api.register(name='person', url='/person/') class PersonView(ResourceView): resource = PersonResource - methods = [Create, Update, Fetch, List] + methods = [Create, Update, Fetch, BulkFetch] # extra resources for testing max_limit class Post10Resource(PostResource): @@ -187,12 +187,12 @@ class Post250Resource(PostResource): @api.register(name='posts10', url='/posts10/') class Post10View(ResourceView): resource = Post10Resource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] @api.register(name='posts250', url='/posts250/') class Post250View(ResourceView): resource = Post250Resource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] # Documents, resources, and views for testing differences between db refs and object ids class A(db.Document): @@ -218,17 +218,17 @@ class CResource(Resource): @api.register(url='/a/') class AView(ResourceView): resource = AResource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] @api.register(url='/b/') class BView(ResourceView): resource = BResource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] @api.register(url='/c/') class CView(ResourceView): resource = CResource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] # Documents, resources, and views for testing method permissions @@ -261,7 +261,7 @@ class FetchOnlyView(ResourceView): @api.register(url='/list_only/') class ListOnlyView(ResourceView): resource = MethodTestResource - methods = [List] + methods = [BulkFetch] @api.register(url='/delete_only/') class DeleteOnlyView(ResourceView): @@ -277,7 +277,7 @@ class ViewMethodTestResource(Resource): @api.register(url='/test_view_method/') class TestViewMethodView(ResourceView): resource = ViewMethodTestResource - methods = [Create, Update, BulkUpdate, Fetch, List, Delete] + methods = [Create, Update, BulkUpdate, Fetch, BulkFetch, Delete] def _dispatch_request(self, *args, **kwargs): super(TestViewMethodView, self)._dispatch_request(*args, **kwargs) @@ -290,7 +290,7 @@ class DateTimeResource(Resource): @api.register(name='datetime', url='/datetime/') class DateTimeView(ResourceView): resource = DateTimeResource - methods = [Create, Update, Fetch, List] + methods = [Create, Update, Fetch, BulkFetch] # Document, resource, and view for testing invalid JSON @@ -303,7 +303,7 @@ class DictDocResource(Resource): @api.register(url='/dict_doc/') class DictDocView(ResourceView): resource = DictDocResource - methods = [Fetch, List, Create, Update] + methods = [Fetch, BulkFetch, Create, Update] if __name__ == "__main__": diff --git a/flask_mongorest/__init__.py b/flask_mongorest/__init__.py index f7da37f2..45ee1ce2 100644 --- a/flask_mongorest/__init__.py +++ b/flask_mongorest/__init__.py @@ -1,6 +1,39 @@ from flask import Blueprint -from flask_mongorest.methods import Create, BulkUpdate, List +from flask_mongorest.methods import * +def register_class(app, klass, **kwargs): + # Construct a url based on a 'name' kwarg with a fallback to the + # view's class name. Note that the name must be unique. + name = kwargs.pop('name', klass.__name__) + view_func = klass.as_view(name) + url = kwargs.pop('url', None) + if not url: + document_name = klass.resource.document.__name__.lower() + url = f'/{document_name}/' + + # Insert the url prefix, if it exists + url_prefix = kwargs.pop('url_prefix', '') + if url_prefix: + url = f'{url_prefix}{url}' + + # Add url rules + klass_methods = set(klass.methods) + if Create in klass_methods and BulkCreate in klass_methods: + raise ValueError('Use either Create or BulkCreate!') + + for x in klass_methods & {Fetch, Update, Delete}: + endpoint = view_func.__name__ + x.__name__ + app.add_url_rule( + f'{url}/', defaults={'short_mime': None}, + view_func=view_func, methods=[x.method], endpoint=endpoint, **kwargs + ) + + for x in klass_methods & {Create, BulkFetch, BulkCreate, BulkUpdate, BulkDelete}: + endpoint = view_func.__name__ + x.__name__ + app.add_url_rule( + url, defaults={'pk': None, 'short_mime': None}, + view_func=view_func, methods=[x.method], endpoint=endpoint, **kwargs + ) class MongoRest(object): def __init__(self, app, **kwargs): @@ -10,26 +43,7 @@ def __init__(self, app, **kwargs): def register(self, **kwargs): def decorator(klass): - # Construct a url based on a 'name' kwarg with a fallback to the - # view's class name. Note that the name must be unique. - name = kwargs.pop('name', klass.__name__) - url = kwargs.pop('url', None) - if not url: - document_name = klass.resource.document.__name__.lower() - url = '/%s/' % document_name - - # Insert the url prefix, if it exists - if self.url_prefix: - url = '%s%s' % (self.url_prefix, url) - - # Add url rules - pk_type = kwargs.pop('pk_type', 'string') - view_func = klass.as_view(name) - if List in klass.methods: - self.app.add_url_rule(url, defaults={'pk': None}, view_func=view_func, methods=[List.method], **kwargs) - if Create in klass.methods or BulkUpdate in klass.methods: - self.app.add_url_rule(url, view_func=view_func, methods=[x.method for x in klass.methods if x in (Create, BulkUpdate)], **kwargs) - self.app.add_url_rule('%s<%s:%s>/' % (url, pk_type, 'pk'), view_func=view_func, methods=[x.method for x in klass.methods if x not in (List, BulkUpdate)], **kwargs) + register_class(self.app, klass, **kwargs) return klass return decorator diff --git a/flask_mongorest/methods.py b/flask_mongorest/methods.py index 5357ab45..e80fd0a1 100644 --- a/flask_mongorest/methods.py +++ b/flask_mongorest/methods.py @@ -1,17 +1,31 @@ +import sys +import inspect + +class Fetch: + method = 'GET' + class Create: method = 'POST' class Update: method = 'PUT' -class BulkUpdate: - method = 'PUT' +class Delete: + method = 'DELETE' -class Fetch: - method = 'GET' -class List: +class BulkFetch: method = 'GET' -class Delete: +class BulkCreate: + method = 'POST' + +class BulkUpdate: + method = 'PUT' + +class BulkDelete: method = 'DELETE' + + +members = inspect.getmembers(sys.modules[__name__], inspect.isclass) +__all__ = [m[0] for m in members] diff --git a/flask_mongorest/operators.py b/flask_mongorest/operators.py index 9e0ed823..85a6000b 100644 --- a/flask_mongorest/operators.py +++ b/flask_mongorest/operators.py @@ -53,6 +53,7 @@ class Operator(object): """Base class that all the other operators should inherit from.""" op = 'exact' + typ = 'string' # Can be overridden via constructor. allow_negation = False @@ -75,20 +76,42 @@ def apply(self, queryset, field, value, negate=False): kwargs = self.prepare_queryset_kwargs(field, value, negate) return queryset.filter(**kwargs) +def try_float(value): + try: + return float(value) + except ValueError: + return value + class Ne(Operator): op = 'ne' class Lt(Operator): op = 'lt' + typ = 'number' + + def prepare_queryset_kwargs(self, field, value, negate): + return {'__'.join(filter(None, [field, self.op])): try_float(value)} class Lte(Operator): op = 'lte' + typ = 'number' + + def prepare_queryset_kwargs(self, field, value, negate): + return {'__'.join(filter(None, [field, self.op])): try_float(value)} class Gt(Operator): op = 'gt' + typ = 'number' + + def prepare_queryset_kwargs(self, field, value, negate): + return {'__'.join(filter(None, [field, self.op])): try_float(value)} class Gte(Operator): op = 'gte' + typ = 'number' + + def prepare_queryset_kwargs(self, field, value, negate): + return {'__'.join(filter(None, [field, self.op])): try_float(value)} class Exact(Operator): op = 'exact' @@ -106,6 +129,7 @@ class IExact(Operator): class In(Operator): op = 'in' + typ = 'array' def prepare_queryset_kwargs(self, field, value, negate): # this is null if the user submits an empty in expression (like @@ -140,6 +164,7 @@ class IEndswith(Operator): class Boolean(Operator): op = 'exact' + typ = 'boolean' def prepare_queryset_kwargs(self, field, value, negate): if value == 'false': diff --git a/flask_mongorest/resources.py b/flask_mongorest/resources.py index d293c057..8312ebba 100644 --- a/flask_mongorest/resources.py +++ b/flask_mongorest/resources.py @@ -16,7 +16,9 @@ DocumentProxy = None SafeReferenceField = None -from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField, GenericReferenceField +from mongoengine.fields import EmbeddedDocumentField, ListField +from mongoengine.fields import GenericReferenceField, ReferenceField +from mongoengine.fields import GenericLazyReferenceField, LazyReferenceField from mongoengine.fields import DictField from cleancat import ValidationError as SchemaValidationError @@ -209,7 +211,8 @@ def get_fields(self): """ return self.fields - def get_optional_fields(self): + @staticmethod + def get_optional_fields(): """ Return a list of fields that can optionally be included in the response (but only if a `_fields` param mentioned them explicitly). @@ -228,15 +231,18 @@ def get_requested_fields(self, **kwargs): include_all = False + # NOTE use list(dict.fromkeys()) below instead of set() to maintain order if 'fields' in kwargs: fields = kwargs['fields'] - all_fields_set = set(fields) + all_fields_set = list(dict.fromkeys(fields)) else: - fields = self.get_fields() - all_fields_set = set(fields) | set(self.get_optional_fields()) + fields = list(self.get_fields()) + all_fields = fields + self.get_optional_fields() + all_fields_set = list(dict.fromkeys(all_fields)) if params and '_fields' in params: - only_fields = set(params['_fields'].split(',')) + params_fields = params['_fields'].split(',') + only_fields = list(dict.fromkeys(params_fields)) if '_all' in only_fields: include_all = True else: @@ -371,6 +377,9 @@ def serialize_field_value(self, obj, field_name, field_instance, field_value, ** field_value is an actual value to be serialized. For other fields, see get_field_value method. """ + if isinstance(field_instance, (LazyReferenceField, GenericLazyReferenceField)): + return field_value and field_value.pk + if isinstance(field_instance, (ReferenceField, GenericReferenceField, EmbeddedDocumentField)): return self.serialize_document_field(field_name, field_value, **kwargs) @@ -382,6 +391,7 @@ def serialize_field_value(self, obj, field_name, field_instance, field_value, ** elif callable(field_instance): return self.serialize_callable_field(obj, field_instance, field_name, field_value, **kwargs) + return field_value def serialize_callable_field(self, obj, field_instance, field_name, field_value, **kwargs): @@ -398,12 +408,13 @@ def serialize_callable_field(self, obj, field_instance, field_name, field_value, else: value = field_instance(obj) if field_name in self._related_resources: + res = self._related_resources[field_name](view_method=self.view_method) if isinstance(value, list): - return [self._related_resources[field_name]().serialize_field(o, **kwargs) for o in value] + return [res.serialize_field(o, **kwargs) for o in value] elif value is None: return None else: - return self._related_resources[field_name]().serialize_field(value, **kwargs) + return res.serialize_field(value, **kwargs) return value def serialize_dict_field(self, field_instance, field_name, field_value, **kwargs): @@ -423,18 +434,27 @@ def serialize_dict_field(self, field_instance, field_name, field_value, **kwargs def serialize_list_field(self, field_instance, field_name, field_value, **kwargs): """Serialize each item in the list separately.""" - return [val for val in [self.get_field_value(elem, field_name, field_instance=field_instance.field, **kwargs) for elem in field_value] if val] + if not field_value: + return [] + + field_values = [] + for elem in field_value: + fv = self.get_field_value( + elem, field_name, field_instance=field_instance.field, **kwargs + ) + if fv is not None: + field_values.append(fv) + + return field_values def serialize_document_field(self, field_name, field_value, **kwargs): """If this field is a reference or an embedded document, either return a DBRef or serialize it using a resource found in `related_resources`. """ if field_name in self._related_resources: - return ( - field_value and - not isinstance(field_value, DBRef) and - self._related_resources[field_name]().serialize_field(field_value, **kwargs) - ) + if field_value and not isinstance(field_value, DBRef): + res = self._related_resources[field_name](view_method=self.view_method) + return res.serialize_field(field_value, **kwargs) else: if DocumentProxy and isinstance(field_value, DocumentProxy): # Don't perform a DBRef isinstance check below since @@ -476,13 +496,16 @@ def serialize(self, obj, **kwargs): renamed_field = self._rename_fields.get(field, field) # if the field is callable, execute it with `obj` as the param + value = None if hasattr(self, field) and callable(getattr(self, field)): value = getattr(self, field)(obj) # if the field is associated with a specific resource (via the # `related_resources` map), use that resource to serialize it if field in self._related_resources and value is not None: - related_resource = self._related_resources[field]() + related_resource = self._related_resources[field]( + view_method=self.view_method + ) if isinstance(value, mongoengine.document.Document): value = related_resource.serialize_field(value) elif isinstance(value, dict): @@ -808,8 +831,9 @@ def get_objects(self, qs=None, qfilter=None): qs = self.apply_ordering(qs, params) # Apply limit and skip to the queryset + bulk_methods = {methods.BulkUpdate, methods.BulkDelete} limit = None - if self.view_method == methods.BulkUpdate: + if self.view_method in bulk_methods: # limit the number of objects that can be bulk-updated at a time qs = qs.limit(self.bulk_update_limit) elif not custom_qs: @@ -826,13 +850,13 @@ def get_objects(self, qs=None, qfilter=None): # Raise a validation error if bulk update would result in more than # bulk_update_limit updates - if self.view_method == methods.BulkUpdate and len(objs) >= self.bulk_update_limit: + if self.view_method in bulk_methods and len(objs) >= self.bulk_update_limit: raise ValidationError({ 'errors': ["It's not allowed to update more than %d objects at once" % self.bulk_update_limit] }) # Determine the value of has_more - if self.view_method != methods.BulkUpdate and self.paginate: + if self.view_method not in bulk_methods and self.paginate: has_more = len(objs) > limit if has_more: objs = objs[:-1] @@ -846,7 +870,7 @@ def get_objects(self, qs=None, qfilter=None): return objs, has_more - def save_related_objects(self, obj, parent_resources=None): + def save_related_objects(self, obj, parent_resources=None, **kwargs): if not parent_resources: parent_resources = [self] else: @@ -881,7 +905,7 @@ def save_related_objects(self, obj, parent_resources=None): def save_object(self, obj, **kwargs): self.save_related_objects(obj, **kwargs) - obj.save() + obj.save(**kwargs) obj.reload() self._dirty_fields = None # No longer dirty. @@ -905,7 +929,7 @@ def create_object(self, data=None, save=True, parent_resources=None): obj = self.document(**update_dict) self._dirty_fields = update_dict.keys() if save: - self.save_object(obj) + self.save_object(obj, force_insert=True) return obj def update_object(self, obj, data=None, save=True, parent_resources=None): @@ -913,7 +937,7 @@ def update_object(self, obj, data=None, save=True, parent_resources=None): if subresource: return subresource.update_object(obj, data=data, save=save, parent_resources=parent_resources) - update_dict = self.get_object_dict(data, update=True) + update_dict = self.get_object_dict(data, update=True) if save else data self._dirty_fields = [] @@ -939,8 +963,8 @@ def update_object(self, obj, data=None, save=True, parent_resources=None): self.save_object(obj) return obj - def delete_object(self, obj, parent_resources=None): - obj.delete() + def delete_object(self, obj, parent_resources=None, skip_post_delete=False): + obj.delete(signal_kwargs={"skip": skip_post_delete}) # Py2/3 compatible way to do metaclasses (or six.add_metaclass) diff --git a/flask_mongorest/utils.py b/flask_mongorest/utils.py index 272473d2..1a7a65cb 100644 --- a/flask_mongorest/utils.py +++ b/flask_mongorest/utils.py @@ -3,6 +3,7 @@ import datetime from bson.dbref import DBRef from bson.objectid import ObjectId +from bson.decimal128 import Decimal128 import mongoengine isbound = lambda m: getattr(m, 'im_self', None) is not None @@ -18,14 +19,17 @@ class MongoEncoder(json.JSONEncoder): def default(self, value, **kwargs): if isinstance(value, ObjectId): return str(value) - if isinstance(value, DBRef): + elif isinstance(value, DBRef): return value.id - if isinstance(value, datetime.datetime): + elif isinstance(value, datetime.datetime): return value.isoformat() - if isinstance(value, datetime.date): + elif isinstance(value, datetime.date): return value.strftime("%Y-%m-%d") - if isinstance(value, decimal.Decimal): + elif isinstance(value, decimal.Decimal): return str(value) + elif isinstance(value, Decimal128): + return str(value.to_decimal()) + return super(MongoEncoder, self).default(value, **kwargs) diff --git a/flask_mongorest/views.py b/flask_mongorest/views.py index 93463b18..73b4d655 100644 --- a/flask_mongorest/views.py +++ b/flask_mongorest/views.py @@ -1,7 +1,10 @@ +import sys import json - +import traceback import mimerender import mongoengine + +from collections import deque from flask import render_template, request from flask.views import MethodView from flask_mongorest import methods @@ -76,14 +79,18 @@ def _dispatch_request(self, *args, **kwargs): try: self._resource = self.requested_resource(request) return super(ResourceView, self).dispatch_request(*args, **kwargs) - except mongoengine.queryset.DoesNotExist as e: - return {'error': 'Empty query: ' + str(e)}, '404 Not Found' - except ValidationError as e: - return e.args[0], '400 Bad Request' - except Unauthorized: - return {'error': 'Unauthorized'}, '401 Unauthorized' - except NotFound as e: + except (ValueError, ValidationError, mongoengine.errors.ValidationError) as e: + return {'error': str(e)}, '400 Bad Request' + except (Unauthorized, mongoengine.errors.NotUniqueError) as e: + return {'error': str(e)}, '401 Unauthorized' + except (NotFound, mongoengine.queryset.DoesNotExist) as e: return {'error': str(e)}, '404 Not Found' + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + tb = traceback.format_exception(exc_type, exc_value, exc_tb) + err = ''.join(tb) + print(err) + return {'error': err}, '500 Internal Server Error' def handle_validation_error(self, e): if isinstance(e, ValidationError): @@ -108,11 +115,12 @@ def get(self, **kwargs): if pk: self._resource.view_method = methods.Fetch else: - self._resource.view_method = methods.List + self._resource.view_method = methods.BulkFetch # Create a queryset filter to control read access to the # underlying objects qfilter = lambda qs: self.has_read_permission(request, qs.clone()) + if pk is None: result = self._resource.get_objects(qfilter=qfilter) @@ -155,11 +163,34 @@ def post(self, **kwargs): raise NotFound("Did you mean to use PUT?") # Set the view_method on a resource instance - self._resource.view_method = methods.Create + raw_data = self._resource.raw_data + if isinstance(raw_data, dict): + # create single object + self._resource.view_method = methods.Create + return self.create_object() + elif isinstance(raw_data, list): + limit = self._resource.bulk_update_limit + if len(raw_data) > limit: + raise ValidationError({ + 'errors': [f"Can only create {limit} documents at once"] + }) + raw_data_deque = deque(raw_data) + self._resource.view_method = methods.BulkCreate + data = [] + while len(raw_data_deque): + self._resource._raw_data = raw_data_deque.popleft() + data.append(self.create_object()) + + count = len(data) + ret = {'data': data, 'count': count} + return ret, '201 Created' + else: + raise ValidationError({'error': 'wrong payload type'}) + def create_object(self): self._resource.validate_request() try: - obj = self._resource.create_object() + obj = self._resource.create_object(save=False) except Exception as e: self.handle_validation_error(e) @@ -167,8 +198,9 @@ def post(self, **kwargs): if not self.has_add_permission(request, obj): raise Unauthorized + self._resource.save_object(obj, force_insert=True) ret = self._resource.serialize(obj, params=request.args) - if isinstance(obj, mongoengine.Document) and self._resource.uri_prefix: + if self._resource.uri_prefix: return ret, "201 Created", {"Location": self._resource._url(str(obj.id))} else: return ret @@ -240,20 +272,55 @@ def put(self, **kwargs): ret = self._resource.serialize(obj, params=request.args) return ret + def delete_object(self, obj, skip_post_delete=False): + """Delete an object""" + # Check if we have permission to delete this object + if not self.has_delete_permission(request, obj): + raise Unauthorized + + try: + self._resource.delete_object(obj, skip_post_delete=skip_post_delete) + except Exception as e: + self.handle_validation_error(e) + + def delete_objects(self, objs): + """Delete each object in the list one by one, and return the total count.""" + nobjs, count = len(objs), 0 + try: + # separately delete last object to send skip signal + for obj in objs[:-1]: + self.delete_object(obj, skip_post_delete=True) + count += 1 + + self.delete_object(objs[-1]) + count += 1 + except ValidationError as e: + e.args[0]['count'] = count + raise e + else: + return {'count': count} + def delete(self, **kwargs): pk = kwargs.pop('pk', None) # Set the view_method on a resource instance - self._resource.view_method = methods.Delete - - obj = self._resource.get_object(pk) + if pk: + self._resource.view_method = methods.Delete + else: + self._resource.view_method = methods.BulkDelete - # Check if we have permission to delete this object - if not self.has_delete_permission(request, obj): - raise Unauthorized + if pk is None: + result = self._resource.get_objects() + if len(result) == 2: + objs, has_more = result + elif len(result) == 3: + objs, has_more, extra = result - self._resource.delete_object(obj) - return {} + return self.delete_objects(objs) + else: + obj = self._resource.get_object(pk) + self.delete_object(obj) + return {'count': 1} # This takes a QuerySet as an argument and then # returns a query set that this request can read diff --git a/requirements.txt b/requirements.txt index d25a6a9a..478c5c53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,7 @@ --e git://github.com/closeio/mongoengine.git#egg=mongoengine-dev --e git://github.com/closeio/flask-mongoengine.git#egg=flask-mongoengine -mimerender -python-dateutil -sphinx -cleancat>=0.3 -Flask>=0.9 -pymongo>=3.4 -flake8 +cleancat==1.0.0 +mongoengine==0.21.0 +flask-mongoengine==1.0.0 +mimerender @ git+https://github.com/tschaume/mimerender@mpcontribs#egg=mimerender-0.6.1 +python-dateutil==2.8.1 +Flask==1.1.2 +pymongo==3.11.1 diff --git a/requirements3.txt b/requirements3.txt deleted file mode 100644 index 42573917..00000000 --- a/requirements3.txt +++ /dev/null @@ -1,10 +0,0 @@ --e git://github.com/closeio/cleancat.git#egg=cleancat-dev -mongoengine -flask-mongoengine -mimerender -python-dateutil -sphinx -Flask>=0.9 -pymongo -flake8 -flake8-bugbear diff --git a/setup.py b/setup.py index 4890a0f7..d49abbba 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +import os from setuptools import setup # Stops exit traceback on tests @@ -7,6 +8,10 @@ except Exception: pass +SETUP_PTH = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(SETUP_PTH, "requirements.txt")) as f: + required = f.read().splitlines() + setup( name='Flask-MongoRest', version='0.2.3', @@ -27,13 +32,7 @@ test_suite='nose.collector', zip_safe=False, platforms='any', - setup_requires=[ - 'Flask-MongoEngine', - 'mimerender', - 'nose', - 'python-dateutil', - 'cleancat' - ], + install_requires=required, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', diff --git a/tests/__init__.py b/tests/__init__.py index c2876e36..9b6b8c4a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -769,7 +769,7 @@ def test_view_methods(self): resp = self.app.get('/test_view_method/') response_success(resp) - self.assertEqual(resp_json(resp), {'method': 'List'}) + self.assertEqual(resp_json(resp), {'method': 'BulkFetch'}) resp = self.app.post('/test_view_method/', data=json.dumps({ 'txt': 'doc2' diff --git a/tox.ini b/tox.ini index dde168a6..b1010184 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,8 @@ [tox] -envlist = py27,py35 +envlist = py39 [testenv] commands=nosetests deps= nose - py27: -rrequirements.txt - py35: -rrequirements3.txt + py39: -rrequirements.txt