Metadata-Version: 2.1
Name: apidecorators
Version: 0.2.5
Summary: API Decorators
Home-page: https://gitlab.com/miguel.alarcos/apidecorators
Author: Miguel Ángel Alarcos Torrecillas
Author-email: miguel.alarcos@gmail.com
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Description-Content-Type: text/markdown
Requires-Dist: PyJWT (>=1.6.4)
Requires-Dist: aiohttp (>=3.4.4)
Requires-Dist: flatten-dict (>=0.0.3.post1)
Requires-Dist: motor (>=2.0.0)

# API Decorators

define REST schemas for aiohttp and mongodb.

`pip install apidecorators`

This API is valid for schemas in this way:

schema = {
    * primitives
    * objects
    * arrays of primitives
    * arrays of objects
}

where objects are objects of primitives and this kind of objects.

Let me explain by examples:

```python
from apidecorators.api import jwt_auth, get, insert, has_role, update, push, pull, \
                validate, get_many, delete, read_access, write_access, collection, aggregate, \
                update_array, get_from_array, public
from apidecorators.fields import all_fields            
from cerberus import Validator

#given this schema

s_budget = {
    '_id': {'type': 'string'},
    'applicant': {'type': 'string'},
    'offerer': {'type': 'string'},
    'description': {'type': 'string', 'required': True},
    'amount': {'type': 'integer'},
    'favorite': {'type': 'boolean'},
    'comment': {'type': 'dict',
                'schema': {
                    "text": {"type": "string"},
                    "date": {"type": "float"}
                }
    }
}

s_demand = {
    '_id': {'type': 'string'},
    'applicant': {'type': 'string'},
    'description': {'type': 'string', 'required': True},
    'location': {'type': 'string'},
    'budgets': {
        'type': 'list',
        'schema': s_budget
    }
}

v_demand = Validator(s_demand)
v_budget = Validator(s_budget)

# we define a POST this way

def set_routes_demand(routes):

    @routes.post('/api/demand') # aiohttp routes
    @jwt_auth # must receive a valid JWT token
    @collection('demand') # which collection
    @write_access({'*': '*'}) # any user can write any field
    @validate(validator=v_demanda) # set the cerberus validator
    @insert # it will be an insert
    async def post_demand(document, request, token):
        document['applicant'] = token['user']
        return document  # the returned document will be written in the collection described above

# we GET a document this way:

    @routes.get('/api/demand/{_id}')
    @jwt_auth
    @collection('demand')
    # the user stores in the field applicant can read all fields minus location
    # any other user can read only description and location
    @read_access({'applicant': all_fields(s_demand) - {'location'}, '*': {'description', 'location'}})
    @get # it will be a get
    async def get_demand(document, token):
        # the last chance to change the document that will be sent to the client    
        return document

# we PUT a document this way:

    @routes.put('/api/demand/{_id}')
    @jwt_auth
    @collection('demand')
    @write_access({'applicant': {'description', 'location'}})
    @validate(update=True, validator=v_demanda) # see the attribute update=True
    @update # it will be an update
    async def put_demand(old_doc, document, request, token):      
        return document

# let see how to push to an array:

    @routes.put('/api/demand/{_id}/budgets')
    @jwt_auth
    @collection('demand')
    @write_access({'*': {'description', 'amount'}})
    @validate(validator=v_budget)
    @push('budgets') # the name of the array
    async def push_budget(old_doc, document, request, token):
        document['offerer'] = token['user']
        document['applicant'] = old_doc['applicant']      
        return document

# update an element of an array

    @routes.put('/api/demand/{_id}/budgets/{sub_id}')
    @jwt_auth
    @collection('demand')
    # the user stores in offerer field of subdocument (sub_id) can update description and amount
    # the user stores in applicant field of subdocument (sub_id) can update favorite and comment
    # if you pass root a value different from '.', that will be the root where to check users of write_access
    @write_access({'offerer': {'description', 'amount'}, 'applicant': {'favorite', 'comment'}}, root='budgets')
    @update_array('budgets')
    async def update_budgets(old_doc, document, token):      
        return document

# get many

    @routes.get('/api/demand')
    @public
    @collection('demand')
    @read_access({'*': '*'})
    @get_many
    async def get_many_demands(col, query, token):  
        applicant = query["applicant"]
        return col.find({"applicant": applicant}).skip(0).limit(10)

# get from an array

    @routes.get('/api/demanddemand/{_id}/budgets')
    @jwt_auth
    @collection('demand')
    @read_access({'offerer': {'description', 'amount'}})
    @get_from_array('budgets')
    async def get_presupuestos(document, token):   
        #the chance to remove empty objects in array   
        return document

# and you can do aggregates

    @routes.get('/api/demand/aggregates/comments')
    @public # it is not restricted by a JWT token, the user will be anonymous
    @collection('demand')
    @aggregate
    async def get_aggr_comments(col, query, token):  
        offerer = query["offerer"]
        pipeline = [
    {"$match": {"budgets.offerer": offerer}},
    {"$unwind": "$budgets"},
    {"$match": {"budgets.offerer": offerer}},
    {"$group": {"_id": "$budgets.offerer", "comments": {"$push": {
        "text": "$budgets.comment.text",
        "date": "$budgets.comment.date",
        "author": "$applicant"
        }}}}
    ]
        return col.aggregate(pipeline)

```

In read_access and write_access you can use dot notation and the $ for the array. Example:

```python
@read_access({'applicant': all_fields(s_demand) | {'budgets.$.comment'}})

```

```python
#app.py
import asyncio
from demand import set_routes_demand
from aiohttp import web
from apidecorators.api import cors_factory


async def handle(loop):
    app = web.Application(loop=loop, middlewares=[cors_factory])
    routes = web.RouteTableDef()

    set_routes_demand(routes)
    app.router.add_routes(routes)
    await loop.create_server(app.make_handler(), '0.0.0.0', 8888)

def main():    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(handle(loop))
    print("Server started at port 8888")
    loop.run_forever()
    loop.close()

if __name__ == '__main__':
    main()
```

docker-compose.yml
```yml
    environment:
    - DB_URI=mongodb://<user>:<password>@url:port/data-base
    - DB=data-base
    - SECRET=secret
```



