Skip to main content

A very micro http framework.

Project description

http://img.shields.io/pypi/v/nanohttp.svg Requirements Status https://travis-ci.org/pylover/nanohttp.svg?branch=master https://coveralls.io/repos/github/pylover/nanohttp/badge.svg?branch=master https://img.shields.io/gitter/room/pylover/nanohttp.svg

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

  • Dispatching arguments using the obj.__annonations__

  • Method(verb) dispatcher.

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

$ pip install nanohttp

From Source

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

Quick Start

demo.py

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>
        """
$ nanohttp demo

Or

from nanohttp import quickstart

quickstart(Root())

WSGI

Do you need a WSGI application?

wsgi.py

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:

gunicorn --reload wsgi:app

Config File

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

debug: true

domain:

cookie:
  http_only: false
  secure: false

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

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:

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

Passing the config file(s) Using python:

from nanohttp import quickstart

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

Command Line Interface

$ 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:

from nanohttp import context

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

Setting cookie:

from nanohttp import context, HttpCookie

context.response_cookies.append(HttpCookie('dummy-cookie1', value='dummy', http_only=True))

Trailing slashes

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

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

Decorators

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:

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:

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:

context.response_content_type = 'application/pdf'

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

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:

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:

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.

from nanohttp import context, RestController

class TipsControllers(RestController):

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

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.

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.

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:

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

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nanohttp-0.8.5.tar.gz (90.6 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page