A powerful yet lean wrapper over Flask to massively speed up API creations and enable super fast module-to-API transformations.
Project description
Symmetric
A powerful yet lean wrapper over Flask to massively speed up API creations and enable super fast module-to-API transformations.
Why Symmetric?
Raw developing speed and ease of use, that's why. While Flask
is a powerful tool to have, getting it to work from scratch can be a bit of a pain, especially if you have never used it before. The idea behind symmetric
is to be able to take any module already written and transform it into a working API in a matter of minutes, instead of having to design the module ground-up to work with Flask
(it can also be used to build an API from scratch really fast). With symmetric
, you will also get some neat features, namely:
- Auto logging.
- Server-side error detection and exception handling.
- Native support for an authentication token on a per-endpoint basis.
- Auto-generated
/docs
endpoint for your API with interactive documentation. - Auto-generated OpenAPI Specification and Markdown documentation files for your API.
Installing
Install using pip!
pip install symmetric
Usage
Running the development server
To start the development server, just run:
symmetric run <module>
Where <module>
is your module name (in the examples, we will be writing in a file named module.py
, so the module name will be just module
). A Flask
instance will be spawned immediately and can be reached at http://127.0.0.1:5000 by default. We don't have any endpoints yet, so we'll add some later. Do not use this in production. The Flask
server is meant for development only. Instead, you can use any WSGI
server to run the API. For example, to run the API using gunicorn, you just need to run gunicorn module:symmetric
and a production ready server will be spawned.
Defining the API endpoints
The module consists of a main object called symmetric
, which includes an important element: the router
decorator. Let's analyze it:
from symmetric import symmetric
@symmetric.router("/some-route", methods=["post"], response_code=200, auth_token=False)
The decorator recieves 4 arguments: the route
argument (the endpoint of the API to which the decorated function will map), the methods
argument (a list of the methods accepted to connect to that endpoint, defaults in only POST
requests), the response_code
argument (the response code of the endpoint if everything goes according to the plan. Defaults to 200
) and the auth_token
argument (a boolean stating if the endpoint requires authentication using a symmetric
token. Defaults to False
).
Now let's imagine that we have the following method:
def some_function():
"""Greets the world."""
return "Hello World!"
To transform that method into an API endpoint, all you need to do is add one line:
@symmetric.router("/sample", methods=["get"])
def some_function():
"""Greets the world."""
return "Hello World!"
Run symmetric run module
and send a GET
request to http://127.0.0.1:5000/sample
. You should get a Hello World!
in response! (To try it with a browser, make sure to run the above command and click this link).
But what about methods with arguments? Of course they can be API'd too! Let's now say that you have the following function:
def another_function(a, b=372):
"""
Adds :a and :b and returns the result of
that operation.
"""
return a + b
To transform that method into an API endpoint, all you need to do, again, is add one line:
@symmetric.router("/add")
def another_function(a, b=372):
"""
Adds :a and :b and returns the result of
that operation.
"""
return a + b
Querying API endpoints
To give parameters to a function, all we need to do is send a json
body with the names of the parameters as keys. Let's see how! Run symmetric run module
and send a POST
request (the default HTTP
method) to http://127.0.0.1:5000/add
, now using the requests
module.
import requests
payload = {
"a": 48,
"b": 21
}
response = requests.post("http://127.0.0.1:5000/add", json=payload)
print(response.json())
We got a 69
response! (48 + 21 = 69
). Of course, you can return dictionaries from your methods and those will get returned as a json
body in the response object automagically!
With this in mind, you can transform any existing project into a usable API very quickly!
The symmetric
token authentication
To speed up your API creation even more, symmetric
includes native support for a simple token authentication. Disclaimer: never use the symmetric
token in production without enforcing HTTPS
. The token travels inside the header of the request, so it wil be visible to anyone sniffing the traffic in your network. The token works like this:
-
Set up the token in the server.
In the environment where your API is going to run, add an environmental variable named
SYMMETRIC_API_KEY
and set its value to be the pre-shared token. If you don't set the environmental key, the defaultSYMMETRIC_API_KEY
value will besymmetric_token
(in your development environment that's probably fine, but in the production server you should never use the default value of thesymmetric
token). -
Force one of your endpoints to use an authentication token.
Let's say your module has a method like this:
def secret_function(): """Greets the world (secretly).""" return "Hello World in secret!"
Add the
symmetric
router decorator in the following manner:@symmetric.router("/secret", methods=["get"], auth_token=True) def secret_function(): """Greets the world (secretly).""" return "Hello World in secret!"
Now, your endpoint won't respond to any request that is not correctly authenticated.
-
Query your endpoint.
To query your endpoint, the request headers must include a key named
symmetric_api_key
with a value to match the one of the environment'sSYMMETRIC_API_KEY
. So, for instance, if you are using the defaultSYMMETRIC_API_KEY
value (symmetric_token
), the request headers for the/secrets
endpoint should be:headers = { "symmetric_api_key": "symmetric_token" }
By sending that payload in the request headers, the endpoint can be accessed correctly.
Changing the default token names
Note that you can change the default client token name and server token name. To change the client token name, run the following command at the start of your module:
symmetric.set_client_token_name("new_client_token_name")
After that, the key of the token in every request header must be new_client_token_name
.
To change the server token name, run the following command at the start of your module:
symmetric.set_server_token_name("NEW_SERVER_TOKEN_NAME")
After that, the key of the token in the server environment must be NEW_SERVER_TOKEN_NAME
.
Auto-generating the API documentation
Generating API documentation is simple with symmetric
. Just run the following command:
symmetric docs <module>
This will automagically generate a json
file documenting the API with an OpenAPI specification. Seems too simple to be true, right? Go ahead, try it yourself! Also, don't be afraid of using type annotations... The annotations will be documented too! They will restrict the parameter types within the OpenAPI generated json
!
You can also generate a more simple and human-readable documentation file with the -m
or the --markdown
flag.
symmetric docs <module> --markdown
This will also automagically generate a markdown file documenting each endpoint with the function docstring, required arguments and more data about that endpoint.
You can also specify the name of the documentation file (defaults to openapi.json
for the default documentation and to documentation.md
for the markdown documentation) using the -f
or the --filename
flag.
ReDoc Documentation
By default, you can GET
the /docs
endpoint (using a browser) to access to interactive auto-generated documentation about your API. It will include request bodies for each endpoint, response codes, authentication required, default values, and much more!
Tip: Given that the ReDoc Documentation is based on the OpenAPI standard, using type annotations in your code will result in a more detailed interactive documentation. Instead of the parameters being allowed to be any type, they will be forced into the type declared in your code. Cool, right?
The whole example
To sum up, if the original module.py
file looked like this before symmetric
:
def some_function():
"""Greets the world."""
return "Hello World!"
def another_function(a, b=372):
"""
Adds :a and :b and returns the result of
that operation.
"""
return a + b
def secret_function():
"""Greets the world (secretly)."""
return "Hello World in secret!"
The complete final module.py
file with symmetric
should look like this:
from symmetric import symmetric
@symmetric.router("/sample", methods=["get"])
def some_function():
"""Greets the world."""
return "Hello World!"
@symmetric.router("/add")
def another_function(a, b=372):
"""
Adds :a and :b and returns the result of
that operation.
"""
return a + b
@symmetric.router("/secret", methods=["get"], auth_token=True)
def secret_function():
"""Greets the world (secretly)."""
return "Hello World in secret!"
To run the server, just run symmetric run module
. Now, you can send POST
requests to http://127.0.0.1:5000/add
and GET
requests to http://127.0.0.1:5000/sample
and http://127.0.0.1:5000/secret
. Here is a simple file to get you started querying your API:
import requests
def call_sample():
response = requests.get("http://127.0.0.1:5000/sample")
return response.text
def call_add():
payload = {
"a": 48,
"b": 21
}
response = requests.post("http://127.0.0.1:5000/add", json=payload)
return response.json()
def call_secret():
headers = {
"symmetric_api_key": "symmetric_token"
}
response = requests.get("http://127.0.0.1:5000/secret", headers=headers)
return response.text
if __name__ == '__main__':
print(call_sample())
print(call_add())
print(call_secret())
Running symmetric docs module
would result in a file openapi.json
being created with the following content:
{
"openapi": "3.0.3",
"info": {
"title": "Module API",
"version": "0.0.1"
},
"paths": {
"/add": {
"post": {
"description": "Adds :a and :b and returns the result of\nthat operation.",
"responses": {
"200": {
"$ref": "#/components/responses/SuccesfulOperation"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"a": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "integer"
},
{
"type": "boolean"
},
{
"type": "array"
},
{
"type": "object"
}
]
},
"b": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "integer"
},
{
"type": "boolean"
},
{
"type": "array"
},
{
"type": "object"
}
],
"default": 372
}
},
"additionalProperties": false
}
}
}
}
}
},
"/sample": {
"get": {
"description": "Greets the world.",
"responses": {
"200": {
"$ref": "#/components/responses/SuccesfulOperation"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/secret": {
"post": {
"description": "Greets the world (secretly).",
"responses": {
"200": {
"$ref": "#/components/responses/SuccesfulOperation"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"401": {
"$ref": "#/components/responses/UnauthorizedError"
}
},
"security": [
{
"APIKeyAuth": []
}
]
}
}
},
"components": {
"securitySchemes": {
"APIKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "symmetric_api_key"
}
},
"responses": {
"SuccesfulOperation": {
"description": "Successful operation"
},
"UnauthorizedError": {
"description": "Invalid or non-existent authentication credentials."
},
"InternalError": {
"description": "Unexpected internal error (API method failed, probably due to a missuse of the underlying function)."
}
}
}
}
Running symmetric docs module --markdown
would result in a file documentation.md
being created with the following content:
# Module API Documentation
Endpoints that require an authentication token should send it in a key named `symmetric_api_key` inside the request headers.
## `/add`
### Description
Adds :a and :b and returns the result of
that operation.
### Metadata
`HTTP` methods accepted: `POST`
Does not require an authentication token.
### Parameters
```py
{
a,
b, # defaults to 372
}
```
## `/sample`
### Description
Greets the world.
### Metadata
`HTTP` methods accepted: `GET`
Does not require an authentication token.
### Parameters
No required parameters.
## `/secret`
### Description
Greets the world (secretly).
### Metadata
`HTTP` methods accepted: `GET`
Requires an authentication token.
### Parameters
No required parameters.
Logging
By default, the logs in the server will be written into the stdout
and into a file named symmetric.log
. You can change the name of the file by specifying the LOG_FILE
environmental variable, if you want to.
Route rules
There are some rules regarding the correct routes that can be used. Failing to follow the symmetric
route rules will result in the API not being run and an error being thrown and logged. To follow the rules, a route:
- Can't be defined twice.
- Can't have repetitions of
/
,-
or_
. - Can't have concatenations of
/
with-
or of_
with-
. - Can't include characters other than letters (uppercase and/or lowercase),
/
,-
and_
. - Can't end with a
/
, a-
or a_
. The only exception of this rule is when the route is just/
, in which case, it can end with/
. - Must start with a
/
.
Here are some examples.
Correct route patterns
/
/symmetric
/hi/hello
/hello-world/basic_syntax
/_element/BIGelement
Incorrect route patterns
/hi//hello
element
/another-element/
/bad-_composition
/-worse
/element__two
/element2
/oof-number-one-
/oof_number_two_
Developing
Clone the repository:
git clone https://github.com/daleal/symmetric.git
cd symmetric
Recreate environment:
./environment.sh
. .venv/bin/activate
Test install:
poetry install # will also install the symmetric CLI
Run the tests:
python -m unittest
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
Built Distribution
Hashes for symmetric-3.3.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f0d8dda900ab0ceca2feaa72c8d68294ea63877d9c7b9a654b050bbc2539fa0d |
|
MD5 | deac3354f84e663dbc81bc8d6834092b |
|
BLAKE2b-256 | 331963244d0f5872a12daa589ff7b425c727f6ed93e658d9ea2bcab273d4bf21 |