Search code examples
pythonpython-3.xflaskflask-restplus

HTTP GET API Design which uses same endpoint but with provides filtered results with request parameters in Flask


I am using flask-restplus to create a RESTful API which connects to a MongoDB collection and provides information according to the API called.

API Design

HTTP GET

api/hashdoc/<str:product_id>

function: provides all a list of the relevant information for the given product_id in the Database.

example response:

[
  {
    "product_id": "ABC",
    "metadata": {
        ...
    },
    "time_info": {
       ....
    }
  },
  {
    "product_id": "ABC",
    "metadata": {
       ...
    },
    "time_info": {
        ...
     }
  }
]

HTTP GET

api/hashdoc/<str:product_id>?from=<str:ISO8601_timestamp>&to=<str:ISO8601_timestamp>

function: Should Provide a JSON response of information filtered using from and to values in the Database

example response:


   {
    "product_id": "ABC",
    "metadata": {
     ...
    },
    "time_info": {
        ...
    }
   }

Current Implementation

Within my hashdoc.py I have the following code:


from flask import request, abort
from flask_restplus import Namespace, Resource, fields, reqparse
from databases.documentdb import mongo

# API
api = Namespace('hashdoc', description='Hash Document Operations')


# REQUEST PARSER HERE
event_duration_parser = reqparse.RequestParser()

event_duration_parser.add_argument('from', type=str, required=True, help='ISO8601 UTC Timestamp (ms precision)')
event_duration_parser.add_argument('to', type=str, required=True, help='ISO8601 UTC Timestamp (ms precision)')

## hash_doc = api.model(....) removed for brewity

@api.route('/<product_id>')
@api.param('product_id', 'EPC Product ID')
@api.response(404, 'No Values Exist for given Product ID.')
class HashDocListResource(Resource):
    @api.marshal_list_with(hash_doc)
    def get(self, product_id):
        '''Fetch Hash Documents for given Product ID'''
        print(product_id)
        product_hash_doc = mongo.db['HashData'].find({'product_id': product_id}, {'_id': 0})
        list_product_hashdoc = list(product_hash_doc)
        if len(list_product_hashdoc):
            return list_product_hashdoc, 200
        else:
            abort(404, {'error': 'No Values Exist for given Product ID.'})


@api.route('/<product_id>')
@api.param('product_id', 'EPC Product ID')
@api.response(404, 'No Values Exist for given Product ID & Time Range')
class HashDocResource(Resource):
    @api.expect(event_duration_parser)
    @api.marshal_with(hash_doc)
    def get(self, product_id):
        args = event_duration_parser.parse_args()
        from_time = args.get('from')
        to_time = args.get('to')

        product_hash_doc_for_time_range = mongo.db['HashData'].find_one(
                        {'product_id': product_id, 
                         # Filtering based on TIMESTAMPS HERE
                        {'_id': 0})

        if product_hash_doc_for_time_range:
            return product_hash_doc_for_time_range, 200
        else:
            abort(404, 'No Values Exist for given Product ID & Time Range')

Tests

with curl I perform the following:

curl -XGET http://localhost:5000/api/hashdoc/ABC

which provides me the list of the hash documents and the behaviour is correct. However when I do the following:

curl -XGET http://localhost:5000/api/hashdoc/ABC?from\="2019-11-05T14:32:31.830000Z"\&to\="2019-11-05T14:46:00.444000Z"

The Flask app still queries the api/hashdoc/ABC and provides me the list and not the filtered document here.

How does one manage such API with request parameters? Does one need to implement them separately?

Does one have to perform a check if the from and to are not None in the same resource class and then perform the query?


Solution

  • The Flask app still queries the api/hashdoc/ABC and provides me the list and not the filtered document here.

    You created two resource classes with same route url. So it uses the one it sees first (which in your case is the HashDocListResource).

    How does one manage such API with request parameters? Does one need to implement them separately?

    No, you don't make separate resource classes just for query params.

    Does one have to perform a check if the from and to are not None in the same resource class and then perform the query?

    Yes.

    I am not sure why you are using find in HashDocListResource but find_one in HashDocResource. Is it that there can only be one HashDoc in given time range?

    Sample implementation of the same.

    from flask import request, abort
    from flask_restplus import Namespace, Resource, fields, reqparse
    from databases.documentdb import mongo
    
    # API
    api = Namespace('hashdoc', description='Hash Document Operations')
    
    
    # REQUEST PARSER HERE
    event_duration_parser = reqparse.RequestParser()
    
    event_duration_parser.add_argument('from', type=str, required=True, help='ISO8601 UTC Timestamp (ms precision)')
    event_duration_parser.add_argument('to', type=str, required=True, help='ISO8601 UTC Timestamp (ms precision)')
    
    ## hash_doc = api.model(....) removed for brewity
    
    @api.route('/<product_id>')
    @api.param('product_id', 'EPC Product ID')
    @api.response(404, 'No Values Exist for given Product ID.')
    class HashDocListResource(Resource):
        @api.expect(event_duration_parser)
        @api.marshal_list_with(hash_doc)
        def get(self, product_id):
            '''Fetch Hash Documents for given Product ID'''
            args = event_duration_parser.parse_args()
            from_time = args.get('from')
            to_time = args.get('to')
    
            query = {
                'product_id': product_id,
            }
            if from_time and to_time:
                query['your_time_stamp_field'] = {
                    '$lte': to_time
                }
                query['your_time_stamp_field'] = {
                    '$gte': from_time
                }
            projection = {'_id': 0}
            product_hash_docs = mongo.db['HashData'].find(query, projection)
            list_product_hashdoc = list(product_hash_docs)
            if len(list_product_hashdoc):
                return list_product_hashdoc, 200
            else:
                abort(404, {'error': 'No Values Exist for given Product ID.'})