# Chocs [![PyPI version](https://badge.fury.io/py/chocs.svg)](https://pypi.org/project/chocs/) ![Release](https://github.com/kodemore/chocs/workflows/Release/badge.svg) ![Linting and Tests](https://github.com/kodemore/chocs/workflows/Linting%20and%20Tests/badge.svg) [![codecov](https://codecov.io/gh/kodemore/chocs/branch/master/graph/badge.svg)](https://codecov.io/gh/kodemore/chocs) [![Maintainability](https://api.codeclimate.com/v1/badges/9e3c979283b2361a9174/maintainability)](https://codeclimate.com/github/kodemore/chocs/maintainability)
Chocs is a modern HTTP framework for building AWS HTTP API/REST API and WSGI compatible applications. 
Chocs aims to be small, expressive, and robust. 
It provides an elegant API for writing fault-proof, extensible microservices.  

## Features

 - AWS Serverless integration
 - Open api integration  
 - Elegant and easy API
 - No additional bloat like built-in template engines, session handlers, etc.
 - Compatible with all WSGI servers
 - Loosely coupled components which can be used separately
 - Multipart body parsing
 - Graceful error handling
 - HTTP middleware support
 - Fast routing

## Installation
```
pip install chocs
```

# Usage

## Quick start

```python
from chocs import Application
from chocs import HttpRequest
from chocs import HttpResponse
from chocs import serve


http = Application()

@http.get("/hello/{name}")
def hello(request: HttpRequest) -> HttpResponse:
    return HttpResponse(f"Hello {request.path_parameters.get('name')}!")

serve(http)
```

 > Keep in mind that the `serve()` function is using the `bjoern` package, so make sure you included it in your project 
 > dependencies before using it. You are able to use any WSGI compatible server.

## Running application with Gunicorn (or any other WSGI server)

```python
# myapp.py
from chocs import Application
from chocs import HttpRequest
from chocs import HttpResponse
from chocs import create_wsgi_handler


http = Application()


@http.get("/hello/{name}*")
def hello(request: HttpRequest) -> HttpResponse:
    return HttpResponse(f"Hello {request.path_parameters.get('name')}!")

app = create_wsgi_handler(http, debug=False)
```

```bash
gunicorn -w 4 myapp:app
```

## Running application in AWS Lambda (Http api or rest api)

`handler.py`
```python
import logging

from chocs import HttpRequest
from chocs import HttpResponse
from chocs import Application

logger = logging.getLogger()
logger.setLevel(logging.INFO)


http = Application()


@http.get("/hello/{name}")
def hello_handler(request: HttpRequest) -> HttpResponse:
    logger.info("Hello AWS!")
    logger.info(request.attributes.get("aws_context"))
    logger.info(request.attributes.get("aws_event"))
    return HttpResponse(f"Hello {request.path_parameters.get('name')}")


__all__ = ["hello_handler"]
```

`serverless.yml`
```yaml
service: aws-hello-name

provider:
  name: aws
  runtime: python3.8

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true

functions:
  hello_name:
    handler: handler.hello_handler
    events:
      - httpApi:
          method: GET
          path: /hello/{name}
```

```bash
serverless deploy
```

## Routing
Chocs is shipped with a built-in routing module. The easiest way to utilise chocs' routing is to use `chocs.router` object.
`chocs.router` is an instance of the module's internal class `chocs.application.Application`, which provides a simple API 
where each function is a decorator corresponding to an HTTP method.

```python
from chocs import Application, HttpResponse, HttpRequest


http = Application()


@http.get("/hello")
def hello(req: HttpRequest) -> HttpResponse:
    ...
```

The above example will assign the hello function to handle a `GET /hello` request. 

Available methods:
- `delete`
- `get`
- `head`
- `options`
- `patch`
- `post`
- `put`
- `trace`

### Parametrized routes

Routes can contain parameterised parts. Parameters must be enclosed within `{` and `}`.

```python
from chocs import Application

http = Application()


@http.get("/pet/{id}")
def hello():
    ...
```
Will match the following URIs:
 - `/pet/1`
 - `/pet/abc`
 - `/pet/abc1`
 
### Wildcard routes

Asterisks (`*`) can be used in the route's pattern to match any possible combination. Keep in mind that routes which 
_do not_ contain wildcards are prioritised over routes with wildcards.

```python
from chocs import Application

http = Application()


@http.get("/pet/*", id)
def hello():
    ...
```

The above example will match following URIs:
- `/pet/a`
- `/pet/a/b/c`
- `/pet/12jd/fds`

### Route groups

Chocs supports route groups. Route groups is implemented through [context lib interface](https://docs.python.org/3/library/contextlib.html).
If you need to split your application in smaller chunks with standalone req/res handlers consider the
following example:

```python
from threading import Thread

from chocs.wsgi import serve 
from chocs import Application
from chocs import HttpRequest
from chocs import HttpResponse

main_app = Application()

with main_app.group("/users/{id}") as user_module:
    
    @user_module.post("/profile_picture")  # POST /users/{id}/profile_pictures
    def create_profile_picture(request: HttpRequest) -> HttpResponse:
        ...
    
    @user_module.get("/profile_picture")  # GET /users/{id}/profile_pictures
    def get_profile_picture(request: HttpRequest) -> HttpResponse:
        ...
    
    @user_module.get("/badges") # GET /users/{id}/badges
    def badges(request: HttpRequest) -> HttpResponse:
        ...

with main_app.group("/payments") as payment_module:

    @payment_module.get("/analytics") # GET /payments/analytics
    def get_analytics(request: HttpRequest) -> HttpResponse:
        ...

if __name__ == '__main__':
    def wsgi_user_module():
        serve(user_module, port=8081)
    def wsgi_payment_module():
        serve(payment_module, port=8082)

    Thread(target=wsgi_user_module).start()
    payment_module()
```

The above example shows how to run two different modules, which support their own routes
on two different ports in the one process.

## Middleware

Middleware are functions or classes that inherit `chocs.Middleware`. Middlewares have access to the request object
and the `next` function which can be used to control middleware stack flow. Successful middleware execution should call
the `next` function which accepts a `chocs.HttpRequest` instance and returns `chocs.HttpReponse`.

Middlewares can perform various tasks:
- Making changes in request/response objects ending
- Validating input data
- Authenticating users
- End request-response cycle
- Connecting to external data sources

## Integration with openapi

To provide automatic validation for your request based on open api specification, 
use `chocs.middleware.OpenApiMiddleware` middleware bundled with chocs:

```python
from chocs.middleware import OpenApiMiddleware
from chocs import Application, HttpRequest, HttpResponse
from os import path
from dataclasses import dataclass

# absolute path to file containing open api documentation; yaml and json files are supported
openapi_filename = path.join(path.dirname(__file__), "/openapi.yml")

# instantiating application and passing open api middleware
app = Application(OpenApiMiddleware(openapi_filename, validate_body=True, validate_query=True))

# defining our dataclass for better type support
@dataclass()
class Pet:
    id: str
    name: str

# the registered route must correspond to open api route within `path` section.
# if request body is invalid the registered function will not be executed
@app.post("/pets") # `parsed_body` parameter can be used to map request to certain type
def create_pet(request: HttpRequest) -> HttpResponse:
    try: 
        pet = Pet(**request.parsed_body)
    except TypeError:
        return HttpResponse(status=400)
    
    return HttpResponse(pet.name)
```

> Complete integration example can be [found here](./examples/input_validation_with_open_api/openapi.yml)

Chocs automatically validates:
 - request body, `application/json` header must be present for successful validation
 - query string parameters
 - request headers

## Mapping request payload to custom dataclasses

```python
from chocs.middleware import ParsedBodyMiddleware
from chocs import Application, HttpRequest, HttpResponse
from dataclasses import dataclass

# You can define whether to use strict mode or not for all defined routes.
app = Application(ParsedBodyMiddleware(strict=False))

@dataclass()
class Pet:
    id: str
    name: str

@app.post("/pets", parsed_body=Pet, strict=False) # you can also override default strict mode
def create_pet(request: HttpRequest) -> HttpResponse:
    pet: Pet = request.parsed_body
    assert isinstance(pet, Pet)
    return HttpResponse(pet.name)
```

> Note: By default chocs works in a strict mode, which means when you map request
> data to your object `__init__` method is called. To override this behaviour set
> `strict` property to false: `@app.post("/pets", parsed_body=Pet, strict=False)`
> or use `strict=False` when initialising middleware: `ParsedBodyMiddleware(strict=False)`


### Handling validation errors with custom middleware

By default, if validation fails users will see `500 response`. This behavior can be changed if custom middleware that
catches validation errors is defined and used in application.

### Defining and using a custom middleware
 
The following code defines simple function middleware to catch validation errors when they appear and notifies users:

```python
from chocs.middleware import OpenApiMiddleware
from chocs.json_schema.errors import ValidationError
from chocs import Application, HttpRequest, HttpResponse
from dataclasses import dataclass
import json
from typing import Callable
from os import path

openapi_filename = path.join(path.dirname(__file__), "/openapi.yml")


# middleware must always accept two parameters; HttpRequest and Callable and return HttpResponse
def handle_errors(request: HttpRequest, next: Callable) -> HttpResponse:
    try:
        return next(request) # we pass request further to middleware pipeline
    except ValidationError as error: # if exception is thrown it is caught here and new response is generated instead
        json_response = {
            "code": error.code,
            "message": str(error),
        }
        return HttpResponse(json.dumps(json_response), status=422)
    
# error handling middleware must go before open api one to catch errors thrown inside open api middleware
app = Application(handle_errors, OpenApiMiddleware(openapi_filename))

@dataclass()
class Pet:
  id: str
  name: str

@app.post("/pets", parsed_body=Pet)
def create_pet(request: HttpRequest) -> HttpResponse:
  assert isinstance(request.parsed_body, Pet)

  pet = request.parsed_body
  return HttpResponse(pet.name)
```

Full working example can be found inside [examples directory](./examples/input_validation_with_open_api)

## Request
`chocs.Request` object is an abstraction around WSGI's environment and `wsgi.input` data with handy interface 
to ease everyday work.

#### `chocs.Request.headers:chocs.HttpHeaders (read-only)`
Keeps parsed headers in dict-like object.

#### `chocs.Request.body:io.BytesIO` 
Raw body data

#### `chocs.Request.parsed_body:chocs.HttpMessage`
Depending on the content type it could be one of the following:
 - `chocs.FormHttpMessage`
 - `chocs.JsonHttpMessage`
 - `chocs.MultipartHttpMessage`
 - `chocs.YamlHttpMessage`

#### `chocs.Request.as_dict(): dict`
Tries to convert request body to a dict and returns it.

> Note this will only work with json and yaml content types.

#### `chocs.Request.as_str(): str`
Returns request content as a string.
 
#### `chocs.Request.cookies:typing.List[chocs.HttpCookie]` 
Request's cookies

#### `chocs.Request.method:chocs.HttpMethod`
The request's method

#### `chocs.Request.path:str`
The request's path

#### `chocs.Request.query_string:chocs.HttpQueryString`
A dict like object with parsed query string with JSON forms support
        
#### `chocs.Request.path_parameters:dict`
Matched route parameters, for example when `/users/john` matches the `/users/{name}` route, parameters will contain a 
`name` key with a value of `john`

#### `chocs.Request.attributes:dict`
Other environmental or custom attributes attached to the request object, eg.: `aws_event` or `aws_context`
when running chocs app as aws lambda.

## Response
`chocs.Response` object is a part of request-response flow and it is required to be returned by all functions
decorated with `router.*` method. Instance of the response class is recognised by `chocs.Application` and used to 
generate real response served to your clients.

#### `chocs.Response.body: io.BytesIO` 
Body served to server's clients.

### `chocs.Response.status_code: chocs.HttpStatus`
Valid response code, instance of `chocs.HttpStatus` enum can be used or just a status code's number.

#### `chocs.Response.cookies:chocs.HttpCookieJar` 
Response's cookies

#### `chocs.Response.write(body: Union[bytes, str, bytearray])`
Write bytes to response body

#### `chocs.Response.close()`
Makes body non-writable.

#### `chocs.Response.writable: bool`
Indicates whether response's body is writable.

#### `chocs.Response.parsed_body:chocs.HttpMessage`
Depending on the content type it could be one of the following:
- `chocs.FormHttpMessage`
- `chocs.JsonHttpMessage`
- `chocs.MultipartHttpMessage`
- `chocs.YamlHttpMessage`

#### `chocs.Response.as_dict(): dict`
Tries to convert response body to a dict and returns it.

> Note this will only work with json and yaml content types.

#### `chocs.Response.as_str(): str`
Returns response content as a string.

## Working with cookies

`chocs.HttpCookieJar` object takes care of cookie handling. It can be accessed in dict-like manner, when item is requested,
instance of `chocs.HttpCookie` is returned to user. 

Cookies can be set either by passing string value to the `chocs.CookieJar`'s key, or by calling `chocs.CookieJar.append` 
method which accepts instance of `chocs.Cookie`.

### Reading client cookies

Cookies can be easily accessed from `chocs.Request.cookies` object which is injected as a parameter to each function 
registered as route handler. Consider the following example:

```python
from chocs import HttpRequest
from chocs import HttpResponse
from chocs import Application
from chocs import serve

http = Application()


@http.get("/cookies")
def read_cookies(request: HttpRequest) -> HttpResponse:

    message = "Hello"
    if "user_name" in request.cookies:
        message += f", {str(request.cookies['user_name'])}"
    message += "!"

    return HttpResponse(body=message)

serve(http)
```

### Setting cookies
```python
from datetime import datetime

from chocs import HttpCookie
from chocs import HttpRequest
from chocs import HttpResponse
from chocs import Application
from chocs import serve

http = Application()


@http.get("/cookies")
def read_cookies(request: HttpRequest) -> HttpResponse:
    response = HttpResponse(body="Hi! I have baked some cookies for ya!")
    response.cookies['simple-cookie'] = "Simple cookie for simple people"
    response.cookies.append(HttpCookie("advanced-cookie", "This cookie will expire in 2021-01-01", expires=datetime(2021, 1, 1)))
    return response

serve(http)
```

# Contributing

## Prerequisites

- libev
- python 3.8
- docker

## Installation

`poetry install`

## Running tests

`poetry run pytest`

## Linting

```shell
poetry run black .
poetry run isort .
poetry run mypy .
```

## PR 
