jlowin / apistar

A smart Web API framework, designed for Python 3. 🌟

Home Page:https://discuss.apistar.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

API Star βœ¨πŸš€βœ¨πŸŒŸ

A smart Web API framework, designed for Python 3.

Build Status codecov Package version Python versions

Community: https://discuss.apistar.org/ πŸ€” πŸ’­ πŸ€“ πŸ’¬ 😎

screenshot


Features

Why should you consider using API Star for your next Web API project?

  • API documentation - Interactive API documentation, that's guaranteed to always be in sync with your codebase.
  • Client libraries - JavaScript and Python client libraries, driven by the typesystems that API Star generates.
  • Schema generation - Support for generating Swagger or RAML API typesystems.
  • Expressive - Type annotated views, that make for expressive, testable code.
  • Performance - Dynamic behaviour for determining how to run each view makes API Star incredibly efficient.
  • Throughput - Support for asyncio to allow for building high-throughput non-blocking applications.

Table of Contents


Quickstart

Install API Star:

$ pip3 install apistar

Create a new project:

$ apistar new .
app.py
tests.py
$ cat app.py
from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls


def welcome(name=None):
    if name is None:
        return {'message': 'Welcome to API Star!'}
    return {'message': 'Welcome to API Star, %s!' % name}


routes = [
    Route('/', 'GET', welcome),
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

app = App(routes=routes)


if __name__ == '__main__':
    app.main()

Run the application:

$ apistar run
Running at http://localhost:8080/

Run the tests:

$ apistar test
tests.py ..
===== 2 passed in 0.05 seconds =====

View the interactive API documentation:

$ open http://localhost:8080/docs/

screenshot

Choosing a framework

API Star allows you to either use a standard threaded WSGI application, or an asyncio application.

WSGI

The benefit of choosing a standard WSGI application is that you'll get better ecosystem support. The SQLAlchemy and Django ORM backends are available, and you can use a large range of existing Python libraries.

To start a new wsgi project use:

$ pip install apistar
$ apistar new .

The application import line in the code will look like this:

from apistar.frameworks.wsgi import WSGIApp as App

ASyncIO

The benefit of an asyncio application is the potential for higher throughput, as you can avoid making thread-blocking calls in favour of co-operative concurrency using async/await. However you'll need to make sure that you only use async components for any blocking operations, such as calls to the database, reading from disk, or making a network request.

To start a new asyncio project use:

$ pip install apistar[asyncio]
$ apistar new . --framework asyncio

The application import line in the code will look like this:

from apistar.frameworks.asyncio import ASyncIOApp as App

You may now include either regular or async handler functions...

def welcome(name=None):
    # A regular handler function that contains no asynchronous operations.
    ...

async def welcome(name=None):
    # An async handler, that may use `async/await` syntax for performing asynchronous operations.
    ...

HTTP

Requests

API Star allows you to dynamically inject various information about the incoming request into your views using type annotation.

from apistar import http

def show_request(request: http.Request):
    return {
        'method': request.method,
        'url': request.url,
        'headers': dict(request.headers)
    }

def show_query_params(query_params: http.QueryParams):
    return {
        'params': dict(query_params)
    }

def show_user_agent(user_agent: http.Header):
    return {
        'user-agent': user_agent
    }

Some of the components you might use most often:

Component Description
http.Request The HTTP request. Includes .method, .url, and .headers attributes.
http.Headers The request headers, returned as a dictionary-like object.
http.Header Lookup a single request header, corresponding to the argument name.
Returns a string or None.
http.QueryParams The request query parameters, returned as a dictionary-like object.
http.QueryParam Lookup a single query parameter, corresponding to the argument name.
Returns a string or None.
http.Body The request body. Returns a bytestring.
http.RequestData The request data, returned as a dictionary-like object.

Responses

By default API star expects view to return plain data, and will return 200 OK responses.

def create_project():
    return {'name': 'new project', 'id': 123}

You can instead set the status code or headers by returning a Response.

def create_project():
    data = {'name': 'new project', 'id': 123}
    headers = {'Location': 'http://example.com/project/123/'}
    return Response(data, status=201, headers=headers)

URL Routing

Use {curly_braces} in your URL conf to include a URL path parameter.

def echo_username(username):
    return {'message': f'Welcome, {username}!'}

app = App(routes=[
    Route('/{username}/', 'GET', echo_username)
])

Use Include to include a list of routes.

user_routes = [
    Route('/', 'GET', list_users),
    Route('/', 'POST', create_user),
    Route('/{user_id}', 'PUT', edit_user),
    Route('/{user_id}', 'DELETE', delete_user)
]

routes = [
    Include('/users', user_routes),
    ...
]

app = App(routes=routes)

Use type annotation on the view method to include typed URL path parameters.

users = {0: 'penny', 1: 'benny', 2: 'jenny'}

def echo_username(user_id: int):
    username = users[user_id]
    return {'message': f'Welcome, {username}!'}

app = App(routes=[
    Route('/{user_id}/', 'GET', echo_username)
])

Parameters which do not correspond to a URL path parameter will be treated as query parameters for GET and DELETE requests, or part of the request body for POST, PUT, and PATCH requests.

def echo_username(username):
    if username is None:
        return {'message': 'Welcome!'}
    return {'message': f'Welcome, {username}!'}

app = App(routes=[
    Route('/hello/', 'GET', echo_username)
])

Reversing URLS

Rather than build URLs by hand, it is possible to generate URLs based on the endpoint, using Router's reverse_url().

from apistar.interfaces import Router

def get_player_details(player_name):
    score = get_score(player_name)
    return {'name': player_name, 'score': score}

def get_all_players(router: Router):
    players = get_players()
    player_list = [
        {
            'name': player.name,
            'url': router.reverse_url('get_player_details', player_name=player.name)
        }
        for player in players
    ]
    return {'players': player_list}

app = App(routes=[
    Route('/players/', 'GET', get_all_players),
    Route('/players/{name}/', 'GET', get_player_details),
])

Command Routing

In addition to routing URLs, you can also route commands, to make additional functionality available directly to the command line client.

For example...

from apistar import Command
from apistar.frameworks.wsgi import WSGIApp as App

...

routes = [
    ...
]

commands = [
    Command('create_user', create_user),
    Command('delete_user', delete_user)
]

app = App(routes=routes, commands=commands)

Type System

API Star comes with a type system that allows you to express constraints on the expected inputs and outputs of your interface.

Here’s a quick example of what the type system in API Star looks like:

from apistar import typesystem

class Rating(typesystem.Integer):
    minimum = 1
    maximum = 5


class ProductSize(typesystem.Enum):
    enum = ['small', 'medium', 'large']


class Product(typesystem.Object):
    properties = {
        'name': typesystem.string(max_length=100),  # Use lowercase functions for inline declarations.
        'rating': Rating,
        'in_stock': typesystem.Boolean,
        'size': ProductSize,
    }

Data Validation

The main benefit of expressing our data constraints in a type system is that we can then use those types as annotations on our handler functions.

def create_product(product: Product):
    ...

routes = [
    Route('/create_product/', 'POST', create_product)
]

Serialization

In addition to using the typesystem types for input validation, you can also use them to serialize the return values of your handler functions.

import typing


def list_products() -> typing.List[Product]:
    queryset = ...  # Query returning products from a data store.
    return [Product(record) for record in queryset]

API Reference

The following typesystem types are currently supported:

String

Validates string data. A subclass of str.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.
  • max_length - A maximum valid length for the data.
  • min_length - A minimum valid length for the data.
  • pattern - A string or compiled regex that the data must match.
  • format - An identifier indicating a complex datatype with a string representation. For example "date", to represent an ISO 8601 formatted date string.
  • trim_whitespace - True if leading and trailing whitespace should be stripped from the data. Defaults to True.

Number

Validates numeric data. A subclass of float.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.
  • maximum - A float representing the maximum valid value for the data.
  • minimum - A float representing the minimum valid value for the data.
  • exclusive_maximum - True for an exclusive maximum limit. Defaults to False.
  • exclusive_minimum - True for an exclusive minimum limit. Defaults to False.
  • multiple_of - A float that the data must be strictly divisible by, in order to be valid.

Integer

Validates integer data. A subclass of int.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.
  • maximum - An int representing the maximum valid value for the data.
  • minimum - An int representing the minimum valid value for the data.
  • exclusive_maximum - True for an exclusive maximum limit. Defaults to False.
  • exclusive_minimum - True for an exclusive minimum limit. Defaults to False.
  • multiple_of - An integer that the data must be strictly divisible by, in order to be valid.

Boolean

Validates boolean input. Returns either True or False.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.

Enum

Validates string input, against a list of valid choices. A subclass of str.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.
  • enum - A list of valid string values for the data.

Object

Validates dictionary or object input. A subclass of dict.

  • default - A default to be used if a field using this typesystem is missing from a parent Object.
  • properties - A dictionary mapping string key names to typesystem or type values.

Note that child properties are considered to be required if they do not have a default value.

Array

Validates list or tuple input. A subclass of list.

  • items - A typesystem or type or a list of typesystems or types.
  • additional_items - Whether additional items past the end of the listed typesystem types are permitted.
  • min_items - The minimum number of items the array must contain.
  • max_items - The maximum number of items the array must contain.
  • unique_items - Whether repeated items are permitted in the array.

Generating API Schemas

API Star is designed to be able to map well onto API description formats, known as "API Schemas".

There is currently provisional support for writing Swagger, RAML, or CoreJSON typesystems. See #69 for more details on work still to be done here.

The default output format is the built-in CoreJSON support:

$ apistar schema
{"_type":"document", ...}

The OpenAPI (Swagger) and RAML codecs are optional, and require installation of additional packages:

Swagger

$ pip install openapi-codec
$ apistar schema --format openapi
{"swagger": "2.0", "info": ...}

RAML

$ pip install raml-codec
$ apistar schema --format raml
#%RAML 0.8
...

Building Websites

Although API Star is designed primarily with Web APIs in mind, it is a general purpose framework, and does also give you the tools you need to build regular websites.

Templates

API Star includes a templating component, that allows you to return templated responses, using Jinja2.

templates/index.html:

<html>
    <body>
        <h1>Hello, {{ username }}</h1>
    </body>
</html>

app.py:

from apistar import Route
from apistar.interfaces import Templates
from apistar.frameworks.wsgi import WSGIApp as App


def hello(username: str, templates: Templates):
    index = templates.get_template('index.html')
    return index.render(username=username)

routes = [
    Route('/', 'GET', hello)
]

settings = {
    'TEMPLATES': {
        'ROOT_DIR': 'templates',     # Include the 'templates/' directory.
        'PACKAGE_DIRS': ['apistar']  # Include the built-in apistar templates.
    }
}

app = App(routes=routes, settings=settings)

Returning a string response from a view will default to using the text/html content type. You can override this by returning a Response, including an explicit Content-Type header.

Static Files

For serving static files, API Star uses whitenoise.

First make sure to install the whitenoise package.

$ pip install whitenoise

Next, you'll then need to include the serve_static handler in your routes. This function expects to take a single URL argument, named path.

from apistar import Route
from apistar.handlers import serve_static

routes = [
    # ...
    Route('/static/{path}', 'GET', serve_static)
]

Finally, include the directory that you'd like to serve static files from in your settings, like so:

settings = {
    'STATICS': {
        'ROOT_DIR': 'statics',       # Include the 'statics/' directory.
        'PACKAGE_DIRS': ['apistar']  # Include the built-in apistar static files.
    }
}

app = App(routes=routes, settings=settings)

Settings & Environment

Application settings

Application settings are configured at the point of instantiating the app.

routes = [
    # ...
]

settings = {
    'TEMPLATES': {
        'ROOT_DIR': 'templates',
        'PACKAGE_DIRS': ['apistar']
    }
}

app = App(routes=routes, settings=settings)

You can include the application settings in a view, by using the Settings type annotation:

from apistar import Settings


def debug_settings(settings: Settings):
    """
    Return a JSON response containing the application settings dictionary.
    """
    return settings

More typically you'll want to include settings into the build method of custom components, so that you can control their initialization, based on the application settings.

Environment

Typically you'll want to follow the "twelve-factor app" pattern and store configuration variables in the environment, rather than keeping them under source control.

API Star provides an Environment class that allows you to load the environment, and ensure that it is correctly configured.

from apistar import environment, typesystem


class Env(environment.Environment):
    properties = {
        'DEBUG': typesystem.boolean(default=False),
        'DATABASE_URL': typesystem.string(default='sqlite://')
    }

env = Env()

Once you have an Environment instance, you can use it when creating the application settings.

settings = {
    'DATABASE': {
        'URL': env['DATABASE_URL']
    }
}

Testing

API Star includes the py.test testing framework. You can run all tests in a tests.py module or a tests/ directory, by using the following command:

$ apistar test

The simplest way to test a view is to call it directly.

from app import hello_world

def test_hello_world():
    assert hello_world() == {"hello": "world"}

There is also a test client, that allows you to make HTTP requests directly to your application, using the requests library.

from app import app
from apistar import TestClient

def test_hello_world():
    client = TestClient(app)
    response = client.get('/hello_world/')
    assert response.status_code == 200
    assert response.json() == {"hello": "world"}

Requests made using the test client may use either relative URLs, or absolute URLs. In either case, all requests will be directed at your application, rather than making external requests.

response = client.get('http://www.example.com/hello_world/')

Backends

SQLAlchemy

API Star has optional support for SQLAlchemy. To use this you first need to install sqlalchemy and your chosen database driver.

$ pip install sqlalchemy
$ pip install psycopg2

Settings

You then need to add the database config to your settings, and install the additional components and commands for SQLAlchemy:

  • URL - The Database URL.
  • METADATA - The SQLAlchemy Metadata instance, typically from the declarative_base.
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.backends import sqlalchemy_backend

Base = declarative_base()

class Customer(Base):
    __tablename__ = "Customer"
    id = Column(Integer, primary_key=True)
    name = Column(String)

routes = [
    # ...
]

# Configure database settings.
settings = {
    "DATABASE": {
        "URL": "postgresql://:@localhost/apistar",
        "METADATA": Base.metadata
    }
}

app = App(
    routes=routes,
    settings=settings,
    commands=sqlalchemy_backend.commands,  # Install custom commands.
    components=sqlalchemy_backend.components  # Install custom components.
)

A few common driver configurations are listed below.

Database Driver URL format
PostgreSQL psycopg2 postgresql://<username>:<password>@localhost/example
MySQL pymysql mysql+pymysql://<username>:<password>@localhost/example
SQLite sqlite3 (Python built-in) sqlite:///example.db

Creating the database tables

Before starting you app you will likely need to create the database tables declared in your MetaData which you can do with the following command:

$ apistar create_tables

Interacting with the database

To interact with the database, use the Session component. This will automatically handle commit/rollback behavior, depending on if the view returns normally, or raises an exception:

from apistar.backends.sqlalchemy_backend import Session

def create_customer(session: Session, name: str):
    customer = Customer(name=name)
    session.add(customer)
    session.flush()  # Flush the changes to the database. This will populate the customer id.
    return {'id': customer.id, 'name': customer.name}

def list_customers(session: Session):
    queryset = session.query(Customer).all()
    return [
        {'id': customer.id, 'name': customer.name}
        for customer in queryset
    ]

To instead access the basic database configuration information in a handler, use the SQLAlchemy component.

This has the following attributes:

  • engine - The global Engine instance.
  • metadata - The MetaData object passed into the settings.
  • Session - A bound sessionmaker factory.

Django ORM

API Star has optional support for Django ORM. To use this you first need to install django and your chosen database driver.

$ pip install django
$ pip install psycopg2

Settings

You then need to add the database config to your settings and the django migration commands:

from apistar.frameworks.wsgi import WSGIApp as App
from apistar.backends import django_orm


routes = [
   # ...
]

# Configure database settings.
settings = {
    'DATABASES': {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': '...',
            'HOST': 'localhost',
            'USER': '...',
            'PASSWORD': ''
        }
    },
    'INSTALLED_APPS': ['project',]
}


app = App(
    routes=routes,
    settings=settings,
    commands=django_orm.commands,  # Install custom commands.
    components=django_orm.components  # Install custom components.
)

Migrations

You also need to manually create the migrations directory inside the project directory.

Before starting you app you will likely need to make migrations and then migrate which you can do with the following commands:

$ apistar makemigrations
$ apistar migrate

Create a new model

To create a new Django model you will want to create a new models.py file and declare it.

from django.db import models

class Customer(models.Model):
    name = models.CharField(max_length=255)

Accessing the database

To interact with the database, use the Session component. This will automatically handle commit/rollback behavior, depending on if the view returns normally, or raises an exception:

from apistar.backends.django_orm import Session

def create_customer(session: Session, name: str):
    customer = session.Customer(name=name)
    customer.save()
    return {'id': customer.id, 'name': customer.name}

def list_customers(session: Session):
    queryset = session.Customer.objects.all()
    return [
        {'id': customer.id, 'name': customer.name}
        for customer in queryset
    ]

Components

You can create new components to inject into your views. For example:

import base64

class User(object):
    """
    A component representing the user that the incoming request is associated with.
    """
    def __init__(self, username):
        self.username = username


def authenticate_user(authorization: http.Header):
    """
    Determine the user associated with a request, using HTTP Basic Authentication.
    """
    if authorization is None:
        return None
    scheme, token = authorization.split()
    if scheme.lower() != 'basic':
        return None
    username, password = base64.b64decode(token).decode('utf-8').split(':')
    return User(username)

Next, register your component with the application:

from apistar import Component

components = [
    Component(User, init=authenticate_user)
]

app = App(
    routes=routes,
    components=components
)

You can then use your component in a view:

def say_hello(user: User):
    return {'hello': user.username}

Component Reference

A complete listing of the available built-in components:

Component Description
http.Method The HTTP method of the request, such as GET.
http.Host The host component of the request URL, such as 'example.com'.
http.Port The port number that the request is made to, such as 443.
http.Scheme The scheme component of the request URL, such as 'https'.
http.Path The path component of the request URL, such as /api/v1/my_view/.
http.QueryString The query component of the request URL, such as page=2.
http.URL The full URL of the request, such as https://example.com/api/v1/my_view/?page=2.
http.Body The body of the request, as a bytestring.
http.QueryParams A multi-dict containing the request query parameters.
http.QueryParam A single request query parameter, corresponding to the keyword argument name. Automatically used for data arguments.
http.Headers A multi-dict containing the request headers parameters.
http.Header A single request query parameter, corresponding to the keyword argument name.
http.Request The full request instance.
interfaces.App The current application.
interfaces.Console The console interface. Supports the .echo(message) interface.
interfaces.CommandLineClient The command line parsing component. Supports the .parse(args) interface.
interfaces.Router The router for the application instance. Supports the reverse_url(name, **kwargs) interface.
interfaces.Schema The CoreAPI schema used to represent the API.
interfaces.StaticFiles The static files component. Supports the get_url(path) interface.
interfaces.Templates The template environment. Supports the get_template(path) interface.
types.KeywordArgs A dictionary containing all the matched URL path arguments, or parsed command line parameters.
types.ParamName A string representing the keyword argument with which a component is being injected into the view. May be for components that vary depending on the parameter name used.
types.PathWildcard A string. May be used for URL path components that should support full wildcard matches, allowing '/' characters.
types.Settings A dictionary containing the application settings.
types.WSGIEnviron A dictionary containing the raw WSGI environ of the incoming request.

Performance

API Star dynamically determines exactly what does and does not need to run for any given view, based on the annotations it includes. This means that it can be incredibly efficient.

For a simple JSON serialization test case, the TechEmpower benchmarks rank API Star as achieving the highest throughput of any Python, JavaScript, Ruby, or Go framework.

Benchmarks

We'll be working towards adding further test case types to the TechEmpower benchmarks in the coming weeks, and including results from both WSGIApp and ASyncIOApp deployment modes.

Its also important to recognize that raw latency or throughput numbers are typically not the most important factor to take into consideration when choosing a framework. Having said that, one aim for API Star is to hit the sweet spot for both performance and for productivity.


Deployment

The Development Server

A development server is available, using the run command:

$ apistar run
# Specify the port or interface via --port and --host
# Serve on port 9001 and use IPv6 only
$ apistar run --port 9001 --host ::1
# If you don't like the Werkzeug web debugger, turn it off
$ apistar run --no-debugger

Running in Production

Running a WSGIApp project

For WSGI applications, the recommended production deployment is Gunicorn, using the Meinheld worker.

$ pip install gunicorn
$ pip install meinheld
$ gunicorn app:app --workers=4 --bind=0.0.0.0:5000 --pid=pid --worker-class=meinheld.gmeinheld.MeinheldWorker

Typically you'll want to run as many workers as you have CPU cores on the server.

Running an ASyncIOApp project

For asyncio applications, use uvicorn.

$ uvicorn app:app --workers=4 --bind=0.0.0.0:5000 --pid=pid

Again, you'll typically want to run as many workers as you have CPU cores on the server.

"Serverless" deployments

API Star can also be deployed on so called "serverless" platforms. A good option for using API Star with this style of deployment is Zappa, which allows you to deploy any Python WSGI server onto AWS Lambda.

Note that only WSGIApp is supported using Zappa. You cannot run an ASyncIOApp under this deployment, as a standard WSGI interface is expected.

For Zappa to execute it needs to be provided with the path to your app instance in its app_function key. Given that your app is contained within app.py, e.g.

# app.py
app = App(routes=routes, settings=settings)

Your zappa_settings.json configuration file should then look something like this:

{
    "dev": {
        "app_function": "app.app",
        "aws_region": "us-east-1",
        "profile_name": "default",
        "s3_bucket": "<a-unique-s3-bucket-name>",
        "keep_warm": false
    },
    "prod": {
        "app_function": "app.app",
        "aws_region": "us-east-1",
        "profile_name": "default",
        "s3_bucket": "<a-unique-s3-bucket-name>",
        "debug": false,
        "log_level": "WARNING",
        "apigateway_description": "Description of your app on AWS API Gateway",
        "lambda_description": "Description of your app on AWS Lambda",
    }
}

See Zappa's installation instructions for full configuration details.

Notes

  • keep_warm is a four minute callback to AWS to ensure your function stays loaded in AWS, decreasing the initial response time. When doing development work you don't really need the function to stay 'warm' 24/7 so by setting it to false in dev it will save you some AWS invocation requests. The free tier at AWS gives you 1,000,000 free requests so it shouldn't matter too much.
  • profile_name specifies which alias to use in your AWS Credentials file. This is usually located at ~/.aws/credentials.
[default]
aws_access_key_id = 'xxx'
aws_secret_access_key = 'xxx'

To successfully run zappa deploy you will need an IAM user on your AWS account with the a sufficiently permissive policy attached. See the discussions on Zappa's minimum policy requirements for more details.


Changelog

0.2 Release

  • Added asyncio support.
  • Added app.main().
  • Added Session support for both SQLAlchemy and DjangoORM backends.
  • Added proper support for registering commands, and using components in command handler functions.
  • Added proper support for registering new components, and separating component interfaces from component implementations.
  • Introduced from apistar.frameworks.wsgi import WSGIApp as App instead of from apistar import App.
  • Introduced from apistar.frameworks.asyncio import ASyncIOApp as App instead of from apistar import App.
  • Changed apistar new --layout [minimal|standard] to apistar new --framework [wsgi|asyncio].
  • The TestClient() class now explicitly requires the app instance to be passed as an argument.
  • Dropped overloaded typesystem classes. Use eg. typesystem.String for declarations and typesystem.string() for inlines.
  • Dropped single-lookup component Template. Just use Templates instead.
  • Dropped single-lookup component Setting. Just use Settings instead.
  • Dropped unneccessary ResponseData annotation.
  • Dropped WSGIResponse. Either return data or a Response.
  • Dropped build() method on components. See the docs for information on creating and registering components.
  • Rationalized the 'TEMPLATES' and 'STATICS' settings.

Development

To work on the API Star codebase, you'll want to clone the repository, and create a Python virtualenv with the project requirements installed:

$ git clone git@github.com:tomchristie/apistar.git
$ cd apistar
$ ./scripts/setup

To run the continuous integration tests and code linting:

$ ./scripts/test
$ ./scripts/lint

API Star is BSD licensed code.
Designed & built in Brighton, England.

β€” ⭐️ β€”

About

A smart Web API framework, designed for Python 3. 🌟

https://discuss.apistar.org/

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Python 74.5%Language:CSS 12.5%Language:HTML 7.8%Language:JavaScript 4.5%Language:Shell 0.7%