Compare commits

..

18 Commits

Author SHA1 Message Date
kennethreitz 4000a6a48c fix 2018-10-15 08:45:22 -04:00
kennethreitz 5b173ed4c4 tour 2018-10-15 08:44:45 -04:00
kennethreitz f56ad73565 tour 2018-10-15 08:42:57 -04:00
kennethreitz 003991c8c6 fix 2018-10-15 08:41:39 -04:00
kennethreitz e2a32afb80 next version 2018-10-15 08:28:10 -04:00
kennethreitz f305a69bb3 tour 2018-10-15 08:26:13 -04:00
kennethreitz 84e8babd9e cleanups 2018-10-15 08:23:38 -04:00
kennethreitz aeb46d9b54 open_api spec 2018-10-15 08:21:40 -04:00
kennethreitz fafe0bd8e4 changelog 2018-10-15 07:19:01 -04:00
kennethreitz 9a2ab45957 safe dump 2018-10-15 07:18:19 -04:00
kennethreitz 66978a8cdc test yaml and form too 2018-10-15 07:14:14 -04:00
kennethreitz 1636012700 json uploads 2018-10-15 07:11:57 -04:00
kennethreitz 09206ae1e4 .pre 2018-10-15 07:05:59 -04:00
kennethreitz 9188475746 remove contributors file 2018-10-15 07:04:30 -04:00
kennethreitz 34d158a632 cleanup homepage 2018-10-15 07:02:53 -04:00
kennethreitz c06e6aa5ca changelog 2018-10-15 07:01:49 -04:00
kennethreitz f4f670f048 fix pipfile and pipfile.lock 2018-10-15 06:56:27 -04:00
kennethreitz 778d742b6e new lockfile 2018-10-15 06:55:31 -04:00
14 changed files with 260 additions and 149 deletions
+1
View File
@@ -13,3 +13,4 @@ build
responder.egg-info/
dist/
app.py
app2.py
+16
View File
@@ -0,0 +1,16 @@
# v0.0.5:
- OpenAPI Schema support.
- Safe load/dump yaml.
# v0.0.4:
- Asyncronous support for data uploads.
- Bug fixes.
# v0.0.3:
- Bug fixes.
# v0.0.2
- Switch to ASGI/Starlette.
# v0.0.1
- Conception!
-4
View File
@@ -1,4 +0,0 @@
- Kenneth Reitz (primary)
- Tom Christie
- Bruno Oliveira
- serhii73
+2 -6
View File
@@ -5,10 +5,7 @@ name = "pypi"
[packages]
responder = {editable = true, path = "."}
uvicorn = "*"
starlette = "*"
aiofiles = "*"
docopt = "*"
[dev-packages]
pytest = "*"
@@ -17,8 +14,7 @@ black = "*"
twine = "*"
flask = "*"
sphinx = "*"
locust = "*"
locustio = "*"
marshmallow = "*"
[requires]
python_version = "3.7"
Generated
+18 -125
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "620c4b82d439a27e1afdbe54a6a77130cc52060846d4a6e322a5b064ff68e8fd"
"sha256": "9b959d9507c521f6088646507633207db03afec6ac31aeab07adf0d737dbb45b"
},
"pipfile-spec": 6,
"requires": {
@@ -21,7 +21,6 @@
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"index": "pypi",
"version": "==0.4.0"
},
"aniso8601": {
@@ -31,6 +30,13 @@
],
"version": "==3.0.2"
},
"apispec": {
"hashes": [
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
],
"version": "==1.0.0b3"
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
@@ -168,10 +174,9 @@
},
"starlette": {
"hashes": [
"sha256:9f42bba2c3140402df7fe645b79aadc694cca80140d7bdd43b8a7175f84a8a70"
"sha256:2c7ec085440fce7146a9be2b6d53b7110c3866ce6fa03d901efdc1fbe97e0f36"
],
"index": "pypi",
"version": "==0.4.1"
"version": "==0.4.2"
},
"urllib3": {
"hashes": [
@@ -184,7 +189,6 @@
"hashes": [
"sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11"
],
"index": "pypi",
"version": "==0.3.12"
},
"websockets": {
@@ -307,7 +311,6 @@
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"markers": "sys_platform == 'win32' and platform_python_implementation == 'CPython'",
"version": "==1.11.5"
},
"chardet": {
@@ -395,59 +398,6 @@
],
"version": "==0.16.0"
},
"gevent": {
"hashes": [
"sha256:1f277c5cf060b30313c5f9b91588f4c645e11839e14a63c83fcf6f24b1bc9b95",
"sha256:298a04a334fb5e3dcd6f89d063866a09155da56041bc94756da59db412cb45b1",
"sha256:30e9b2878d5b57c68a40b3a08d496bcdaefc79893948989bb9b9fab087b3f3c0",
"sha256:33533bc5c6522883e4437e901059fe5afa3ea74287eeea27a130494ff130e731",
"sha256:3f06f4802824c577272960df003a304ce95b3e82eea01dad2637cc8609c80e2c",
"sha256:419fd562e4b94b91b58cccb3bd3f17e1a11f6162fca6c591a7822bc8a68f023d",
"sha256:4ea938f44b882e02cca9583069d38eb5f257cc15a03e918980c307e7739b1038",
"sha256:51143a479965e3e634252a0f4a1ea07e5307cf8dc773ef6bf9dfe6741785fb4c",
"sha256:5bf9bd1dd4951552d9207af3168f420575e3049016b9c10fe0c96760ce3555b7",
"sha256:6004512833707a1877cc1a5aea90fd182f569e089bc9ab22a81d480dad018f1b",
"sha256:640b3b52121ab519e0980cb38b572df0d2bc76941103a697e828c13d76ac8836",
"sha256:6951655cc18b8371d823e81c700883debb0f633aae76f425dfeb240f76b95a67",
"sha256:71eeb8d9146e8131b65c3364bb760b097c21b7b9fdbec91bf120685a510f997a",
"sha256:7c899e5a6f94d6310352716740f05e41eb8c52d995f27fc01e90380913aa8f22",
"sha256:8465f84ba31aaf52a080837e9c5ddd592ab0a21dfda3212239ce1e1796f4d503",
"sha256:99de2e38dde8669dd30a8a1261bdb39caee6bd00a5f928d01dfdb85ab0502562",
"sha256:9fa4284b44bc42bef6e437488d000ae37499ccee0d239013465638504c4565b7",
"sha256:a1beea0443d3404c03e069d4c4d9fc13d8ec001771c77f9a23f01911a41f0e49",
"sha256:a66a26b78d90d7c4e9bf9efb2b2bd0af49234604ac52eaca03ea79ac411e3f6d",
"sha256:a94e197bd9667834f7bb6bd8dff1736fab68619d0f8cd78a9c1cebe3c4944677",
"sha256:ac0331d3a3289a3d16627742be9c8969f293740a31efdedcd9087dadd6b2da57",
"sha256:d26b57c50bf52fb38dadf3df5bbecd2236f49d7ac98f3cf32d6b8a2d25afc27f",
"sha256:fd23b27387d76410eb6a01ea13efc7d8b4b95974ba212c336e8b1d6ab45a9578"
],
"version": "==1.3.7"
},
"greenlet": {
"hashes": [
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"
],
"markers": "platform_python_implementation == 'CPython'",
"version": "==0.4.15"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
@@ -475,27 +425,20 @@
],
"version": "==2.10"
},
"locust": {
"hashes": [
"sha256:aaa38b525795e9c1a35ac3620543cf4e62f82948714f60a32023ea8c9b8edc2e"
],
"index": "pypi",
"version": "==0.0"
},
"locustio": {
"hashes": [
"sha256:be7b44468b8683def983e7451ab505cd85fff8d06f6b75ad7c899cedbbf789ac",
"sha256:c77b471e0e08e215c93a7af9a95b79193268072873fbbc0effca40f3d9b58be4"
],
"index": "pypi",
"version": "==0.9.0"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:1ca4820515332fe61cd83551afd791dd0ee16fc70cf57883f6735e5b1d9d50ed",
"sha256:e076fae11bcdd6ee94b2c78d670c2ca35583dd97cc5f1d646b851c9f53368f0a"
],
"index": "pypi",
"version": "==3.0.0b17"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@@ -511,26 +454,6 @@
],
"version": "==4.3.0"
},
"msgpack": {
"hashes": [
"sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc",
"sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b",
"sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3",
"sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0",
"sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee",
"sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42",
"sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c",
"sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f",
"sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172",
"sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4",
"sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4",
"sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf",
"sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00",
"sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0",
"sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e"
],
"version": "==0.5.6"
},
"packaging": {
"hashes": [
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
@@ -608,36 +531,6 @@
],
"version": "==2018.5"
},
"pyzmq": {
"hashes": [
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
],
"version": "==17.1.2"
},
"readme-renderer": {
"hashes": [
"sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2",
+1
View File
@@ -22,6 +22,7 @@
}
pre,
.pre,
.class em,
.descname,
.method em {
-10
View File
@@ -96,21 +96,11 @@ User Guides
Installing Responder
--------------------
Install the latest release:
.. code-block:: shell
$ pipenv install responder
✨🍰✨
Or, install from the development branch:
.. code-block:: shell
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
Only **Python 3.6+** is supported.
+52
View File
@@ -65,6 +65,58 @@ Or, request YAML back::
>>> print(r.text)
data: {hello: Hello john}
OpenAPI Schema Support
----------------------
Responder comes with built-in support for OpenAPI::
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", version="1.0", openapi="3.0")
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
::
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
info: {title: Web Service, version: 1.0}
openapi: '3.0'
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref = "#/components/schemas/Pet"}
tags: []
HSTS (Redirect to HTTPS)
------------------------
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.0.4"
__version__ = "0.0.5"
+74 -1
View File
@@ -11,6 +11,9 @@ from graphql_server import encode_execution_results, json_encode, default_format
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
from . import models
from . import status_codes
@@ -31,12 +34,24 @@ class API:
status_codes = status_codes
def __init__(
self, static_dir="static", templates_dir="templates", enable_hsts=False
self,
*,
title=None,
version=None,
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
templates_dir="templates",
enable_hsts=False,
):
self.title = title
self.version = version
self.openapi_version = openapi
self.static_dir = Path(os.path.abspath(static_dir))
self.static_route = f"/{static_dir}"
self.templates_dir = Path(os.path.abspath(templates_dir))
self.routes = {}
self.schemas = {}
self.hsts_enabled = enable_hsts
self.static_files = StaticFiles(directory=str(self.static_dir))
@@ -52,6 +67,34 @@ class API:
self._session = None
self.background = BackgroundQueue()
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
@property
def _apispec(self):
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=[MarshmallowPlugin()],
)
for route in self.routes:
if self.routes[route].description:
operations = yaml_utils.load_operations_from_docstring(
self.routes[route].description
)
spec.add_path(path=route, operations=operations)
for name, schema in self.schemas.items():
spec.definition(name, schema=schema)
return spec
@property
def openapi(self):
return self._apispec.to_yaml()
def __call__(self, scope):
path = scope["path"]
root_path = scope.get("root_path", "")
@@ -73,6 +116,32 @@ class API:
return asgi
def add_schema(self, name, schema, check_existing=True):
"""Adds a mashmallow schema to the API specification."""
if check_existing:
assert name not in self.schemas
self.schemas[name] = schema
def schema(self, name, **options):
"""Decorator for creating new routes around function and class definitions.
Usage::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
"""
def decorator(f):
self.add_schema(name=name, schema=f, **options)
return f
return decorator
def path_matches_route(self, path):
"""Given a path portion of a URL, tests that it matches against any registered route.
@@ -156,6 +225,10 @@ class API:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
resp.content = self.openapi
def redirect(
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
):
+1 -1
View File
@@ -10,7 +10,7 @@ async def format_form(r, encode=False):
async def format_yaml(r, encode=False):
if encode:
r.headers.update({"Content-Type": "application/x-yaml"})
return yaml.dump(r.media)
return yaml.safe_dump(r.media)
else:
return yaml.safe_load(await r.content)
+5 -1
View File
@@ -3,7 +3,7 @@ from parse import parse, search
def memoize(f):
def helper(self, s):
memoize_key = f'{f.__name__}:{s}'
memoize_key = f"{f.__name__}:{s}"
if memoize_key not in self._memo:
self._memo[memoize_key] = f(self, s)
return self._memo[memoize_key]
@@ -28,6 +28,10 @@ class Route:
# Strings.
return self.does_match(other)
@property
def description(self):
return self.endpoint.__doc__
@property
def has_parameters(self):
return all([("{" in self.route), ("}" in self.route)])
+2
View File
@@ -35,6 +35,8 @@ required = [
"rfc3986",
"python-multipart",
"chardet",
"apispec",
"marshmallow",
]
+87
View File
@@ -234,3 +234,90 @@ def test_graphql_schema_json_query(api, schema):
r = api.session().post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_json_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), json=dump)
assert r.json() == dump
def test_yaml_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(
api.url_for(route),
data=yaml.dump(dump),
headers={"Content-Type": "application/x-yaml"},
)
assert r.json() == dump
def test_form_uploads(api, session):
@api.route("/")
async def route(req, resp):
resp.media = await req.media()
dump = {"complicated": "times"}
r = session.post(api.url_for(route), data=dump)
assert r.json() == dump
def test_json_downloads(api, session):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/json"})
assert r.json() == dump
def test_yaml_downloads(api, session):
dump = {"testing": "123"}
@api.route("/")
def route(req, resp):
resp.media = dump
r = session.get(api.url_for(route), headers={"Content-Type": "application/x-yaml"})
assert yaml.safe_load(r.content) == dump
def test_schema_generation():
import responder
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0")
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.session().get("http://;/schema.yml")
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0"