nanohttp
========

.. image:: http://img.shields.io/pypi/v/nanohttp.svg
   :target: https://pypi.python.org/pypi/nanohttp

.. image:: https://travis-ci.org/Carrene/nanohttp.svg?branch=master
   :target: https://travis-ci.org/Carrene/nanohttp

.. image:: https://coveralls.io/repos/github/Carrene/nanohttp/badge.svg?branch=master
   :target: https://coveralls.io/github/Carrene/nanohttp?branch=master

.. image:: https://badges.gitter.im/Carrene/nanohttp.svg
   :alt: Join the chat at https://gitter.im/Carrene/nanohttp
   :target: https://gitter.im/Carrene/nanohttp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

A very micro HTTP framework.

Features
--------

- Very simple, less-code & fast
- Using object dispatcher instead of regex route dispatcher.
- Url-Encoded, Multipart and JSON form parsing.
- No ``request`` and or ``response`` objects is available, everything is combined in ``nanohttp.context``.
- A very flexible configuration system: `pymlconf <https://github.com/pylover/pymlconf>`_
- Method(verb) dispatcher.
- Regex route dispatcher.
- Powerful validation mechanism.
- Use Python's `keywordonly` arguments for query strings >= `0.24.0`

Roadmap
-------

The road map is to keep it simple with 100% code coverage. no built-in template engine and or ORM will be included.


Install
-------

PyPI
^^^^

.. code-block:: bash

   $ pip install nanohttp


From Source
^^^^^^^^^^^

.. code-block:: bash

   $ cd path/to/nanohttp
   $ pip install -e .


Quick Start
-----------

``demo.py``

.. code-block:: python

   from nanohttp import Controller, RestController, context, html, json, HttpFound


   class TipsControllers(RestController):
       @json
       def get(self, tip_id: int = None):
           if tip_id is None:
               return [dict(id=i, title="Tip %s" % i) for i in range(1, 4)]
           else:
               return dict(
                   id=tip_id,
                   title="Tip %s" % tip_id
               )

       @json
       def post(self, tip_id: int = None):
           tip_title = context.form.get('title')
           print(tip_id, tip_title)

           # Updating the tips title
           # TipStore.get(tip_id).update(tip_title)
           raise HttpFound('/tips/')


   class Root(Controller):
       tips = TipsControllers()

       @html
       def index(self):
           yield """
           <html><head><title>nanohttp Demo</title></head><body>
           <form method="POST" action="/tips/2">
               <input type="text" name="title" />
               <input type="submit" value="Update" />
           </form>
           </body></html>
           """


.. code-block:: bash
    
   $ nanohttp demo

Or

.. code-block:: python
    
   from nanohttp import quickstart, configure

   configure()
   quickstart(Root())


WSGI
----

Do you need a ``WSGI`` application?

``wsgi.py``

.. code-block:: python

   from nanohttp import configure, Application

   configure(init_value='<yaml config string>', files=['path/to/config.file', '...'], dirs=['path/to/config/directory', '...'])
   app = Application(root=Root())
   # Pass the ``app`` to any ``WSGI`` server you want.


Serve it by gunicorn:

.. code-block:: bash

   gunicorn --reload wsgi:app


Config File
-----------

Create a ``demo.yml`` file. The file below is same as the default configuration.

.. code-block:: yaml

   debug: true

   domain:

   cookie:
     http_only: false
     secure: false


You may use ``nanohttp.settings`` anywhere to access the config values.

.. code-block:: python

   from nanohttp import Controller, html, settings

   class Root(Controller):

       @html
       def index(self):
           yield '<html><head><title>nanohttp demo</title></head><body>'
           yield '<h2>debug flag is: %s</h2>' % settings.debug
           yield '</body></html>'

Passing the config file(s) using command line:

.. code-block:: bash

   $ nanohttp -c demo.yml [-c another.yml] demo


Passing the config file(s) Using python:

.. code-block:: bash

   from nanohttp import quickstart

   quickstart(Root(), config='<YAML config string>')


Command Line Interface
----------------------

.. code-block:: bash

   $ nanohttp -h

   usage: nanohttp [-h] [-c CONFIG_FILE] [-d CONFIG_DIRECTORY] [-b {HOST:}PORT]
                   [-C DIRECTORY] [-V]
                   [{MODULE{.py}}{:CLASS}]

   positional arguments:
     {MODULE{.py}}{:CLASS}
                           The python module and controller class to launch.
                           default is python built-in's : `demo_app`, And the
                           default value for `:CLASS` is `:Root` if omitted.

   optional arguments:
     -h, --help            show this help message and exit
     -c CONFIG_FILE, --config-file CONFIG_FILE
                           This option may be passed multiple times.
     -d CONFIG_DIRECTORY, --config-directory CONFIG_DIRECTORY
                           This option may be passed multiple times.
     -b {HOST:}PORT, --bind {HOST:}PORT
                           Bind Address. default: 8080
     -C DIRECTORY, --directory DIRECTORY
                           Change to this path before starting the server default
                           is: `.`
     -V, --version         Show the version.


Cookies
-------

Accessing the request cookies:


.. code-block:: python

   from nanohttp import context

   counter = context.cookies.get('counter')

Setting cookie:

.. code-block:: python

   from nanohttp import context

   context.cookies['dummy-cookie1'] = 'dummy-value'
   context.cookies['dummy-cookie1']['http_only'] = True

For more information on how to use cookies, please check the python builtin's 
`http.cookies <https://docs.python.org/3/library/http.cookies.html>`_.


Trailing slashes
----------------

If the ``Controller.__remove_trailing_slash__`` is ``True``, then all trailing slashes are ignored.

.. code-block:: python

   def test_trailing_slash(self):
       self.assert_get('/users/10/jobs/', expected_response='User: 10\nAttr: jobs\n')

Decorators to indicate http handlers
------------------------------------

Available decorators are: ``action``, ``html``, ``text``, ``json``, ``xml``, ``binary``

Those decorators are useful to encapsulate response preparation such as setting ``Content-Type`` HTTP header.

Take a look at the code of the ``action`` decorator, all other decorators are derived from this:


.. code-block:: python

   def action(*verbs, encoding='utf-8', content_type=None, inner_decorator=None):
       def _decorator(func):

           if inner_decorator is not None:
               func = inner_decorator(func)

           func.__http_methods__ = verbs if verbs else 'any'

           func.__response_encoding__ = encoding

           if content_type:
               func.__content_type__ = content_type

           return func

       if verbs and callable(verbs[0]):
           f = verbs[0]
           verbs = tuple()
           return _decorator(f)
       else:
           return _decorator

Other decorators are defined using ``functools.partial``:

.. code-block:: python

   html = functools.partial(action, content_type='text/html')
   text = functools.partial(action, content_type='text/plain')
   json = functools.partial(action, content_type='application/json', inner_decorator=jsonify)
   xml = functools.partial(action, content_type='application/xml')
   binary = functools.partial(action, content_type='application/octet-stream', encoding=None)

Of-course, you can set the response content type using:

.. code-block:: python

   context.response_content_type = 'application/pdf'

Of-course, you can define your very own decorator to make your code DRY:

.. code-block:: python

   import functools
   from nanohttp import action, RestController

   pdf = functools.partial(action, content_type='application/pdf')

   class MyController(RestController)

       @pdf
       def get(index):
           .......


Serving Static file(s)
----------------------

The ``nanohttp.Static`` class is responsible to serve static files:

.. code-block:: python

   from nanohttp import Controller, Static

   class Root(Controller):
       static = Static('path/to/static/directory', default_document='index.html')

Then you can access static files on ``/static/filename.ext``

A simple way to run server and only serve static files is:

.. code-block:: bash

   cd path/to/static/directory
   nanohttp :Static


Accessing request payload
-------------------------

The `context.form` is a dictionary representing the request payload, supported request formats are ``query-string``,
``multipart/form-data``, ``application/x-www-form-urlencoded`` and ``json``.

.. code-block:: python

   from nanohttp import context, RestController

   class TipsControllers(RestController):

       @json
       def post(self, tip_id: int = None):
           tip_title = context.form.get('title')


Validating request
------------------

A decorator named: `validate` is available to ensure the request parameters.

.. code-block:: python

   from nanohttp import validate

   ...

   @validate(field1=dict(required=True, min=20, max=100, type_=int, ... ))
   def index(self):
       ...


A complete list of validation options is:

- ``required``: Boolean, indicates the field is required.
- ``type_``: A callable to pass the received value to it as the only argument and get it in the
  apprpriate type, Both ``ValueError`` and ``TypeError`` may be raised if the value cannot casted to
  the specified type. A good example of this callable would be the ``int``.
  
- ``minimum``: Numeric, Minimum allowed value.
- ``maximum``: Numeric, Maximum allowed value.
- ``pattern``: Regex pattern to match the value.
- ``min_length``: Only for strings, the minumum allowed length of the value.
- ``max_length``: Only for strings, the maximum allowed length of the value.

Values for those options can be a pair of ``criteria, http status``, for example:

.. code-block:: python

   @validate(field1=dict(
       required=(True, '400 Bad Request'), 
       min=(20, '471 Minimum allowed value is 20'),
       max=(100, '472 Maximum allowed value is 100'),
       type_=(int, '470 Only integers are allowed here')
   )
   def index(self):
       ...



Dispatcher
----------

The requested path will be split-ed by ``/`` and python's ``getattr`` will be used on the ``Root`` controller
recursively to find specific callable to handle request.

.. code-block:: python

   from nanohttp import RestController

   class Nested(RestController):
       pass

   class Root()
       children = Nested()

Then you can access methods on nested controller using: ``http://host:port/children``

On the ``RestController`` dispatcher tries to dispatch request using HTTP method(verb) at first.


Context
-------

The ``context`` object is a proxy to an instance of ``nanohttp.Context`` which is ``unique per request``.

.. TODO: ADD link to documentation

Hooks
-----

A few hooks are available in ``Controller`` class: ``begin_request``, ``begin_response``,
``end_response``.

For example this how I detect JWT token and refresh it if possible:


.. code-block:: python

   from nanohttp import Application, Controller, context

   class JwtApplication(Application):
       token_key = 'HTTP_AUTHORIZATION'
       refresh_token_cookie_key = 'refresh-token'

       def begin_request(self):
           if self.token_key in context.environ:
               encoded_token = context.environ[self.token_key]
               try:
                   context.identity = JwtPrincipal.decode(encoded_token)
               except itsdangerous.SignatureExpired as ex:
                   refresh_token_encoded = context.cookies.get(self.refresh_token_cookie_key)
                   if refresh_token_encoded:
                       # Extracting session_id
                       session_id = ex.payload.get('sessionId')
                       if session_id:
                           context.identity = new_token = self.refresh_jwt_token(refresh_token_encoded, session_id)
                           if new_token:
                               context.response_headers.add_header('X-New-JWT-Token', new_token.encode().decode())

               except itsdangerous.BadData:
                   pass

           if not hasattr(context, 'identity'):
               context.identity = None

Rendering templates
-------------------

This is how to use mako template engine with the nanohttp:


main.py


.. code-block:: python

   import functools
   from os.path import dirname, abspath, join

   from mako.lookup import TemplateLookup

   from nanohttp import Controller, context, Static, settings, action


   here = abspath(dirname(__file__))
   lookup = TemplateLookup(directories=[join(here, 'templates')])


   def render_template(func, template_name):

       @functools.wraps(func)
       def wrapper(*args, **kwargs):

           result = func(*args, **kwargs)
           if hasattr(result, 'to_dict'):
               result = result.to_dict()
           elif not isinstance(result, dict):
               raise ValueError('The result must be an instance of dict, not: %s' % type(result))

           template_ = lookup.get_template(template_name)
           return template_.render(**result)

       return wrapper


   template = functools.partial(action, content_type='text/html', inner_decorator=render_template)


   class Root(Controller):
       static = Static(here)

       @template('index.mak')
       def index(self):
           return dict(
               settings=settings,
               environ=context.environ
           )


templates/index.html

.. code-block:: html

   <html>
   <head>
       <title>nanohttp mako example</title>
   </head>
   <body>
       <h1>WSGI environ</h1>
       <ul>
       %for key, value in environ.items():
         <li><b>${key}:</b> ${value}</li>
       %endfor
       </ul>
   </body>
   </html>

