Compare commits

..

1 Commits

Author SHA1 Message Date
taoufik 8a46a87b3e Fix missing openapi title, version and openapi_version and black 2019-12-03 19:38:12 +01:00
135 changed files with 3867 additions and 5276 deletions
-3
View File
@@ -1,3 +0,0 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
-16
View File
@@ -1,16 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
-42
View File
@@ -1,42 +0,0 @@
name: "Documentation"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
documentation:
name: "Documentation"
runs-on: ubuntu-latest
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-dependency-glob: |
pyproject.toml
- name: Install package and documentation dependencies
run: uv pip install '.[docs]'
- name: Build static HTML documentation
run: sphinx-build -W --keep-going docs/source docs/build
-56
View File
@@ -1,56 +0,0 @@
name: "Tests"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [
"3.9",
"3.10",
"3.11",
"3.12",
"3.13",
]
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
cache-dependency-glob: |
pyproject.toml
- name: Install package
run: uv pip install '.[develop,test]'
- name: Run tests
run: pytest
-2
View File
@@ -1,4 +1,3 @@
.venv*
.vscode/ .vscode/
.cache .cache
.idea .idea
@@ -7,7 +6,6 @@
.pytest_cache .pytest_cache
.DS_Store .DS_Store
coverage.xml coverage.xml
.coverage*
__pycache__ __pycache__
tests/__pycache__ tests/__pycache__
-33
View File
@@ -1,33 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# Details
# - https://docs.readthedocs.io/en/stable/config-file/v2.html
# Required
version: 2
build:
os: "ubuntu-24.04"
tools:
python: "3.12"
python:
install:
- method: pip
path: .
extra_requirements:
- docs
sphinx:
configuration: docs/source/conf.py
# Use standard HTML builder.
builder: html
# Fail on all warnings to avoid broken references.
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF
#formats:
# - pdf
+18
View File
@@ -0,0 +1,18 @@
# travis use trusty by default
dist: xenial
language: python
python:
- 3.6
- 3.7
- "3.8-dev"
# command to install dependencies
install:
- pip install pipenv --upgrade-strategy=only-if-needed
- pipenv install --dev
# command to run the dependencies
script:
- black responder tests setup.py --check
- pytest
+47 -195
View File
@@ -1,422 +1,274 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [v3.0.0] - 2026-03-22
### Added
- Platform: Added support for Python 3.10 - Python 3.13
- CLI: `responder run` now also accepts a filesystem path on its `<target>`
argument, enabling usage on single-file applications.
- CLI: `responder run` now also accepts URLs.
### Changed
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
- Moved `apispec` and `marshmallow` to `openapi` optional extra
- Replaced `rfc3986` with stdlib `urllib.parse`
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
- Switched from WhiteNoise to ServeStatic
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the `responder.ext` module namespace.
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
### Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated `resume_incomplete` and `resume`
aliases for HTTP 308 (marked for removal in 3.0)
- CLI: `responder run --build` ceased to exist
### Fixed
- Routing: Fixed dispatching `static_route=None` on Windows
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
- Tests: Fixed deprecated httpx TestClient usage
## [v2.0.5] - 2019-12-15
### Added
- Update requirements to support python 3.8
## [v2.0.4] - 2019-11-19 ## [v2.0.4] - 2019-11-19
### Fixed ### Fixed
- Fix static app resolving - Fix static app resolving
## [v2.0.3] - 2019-09-20 ## [v2.0.3] - 2019-09-20
### Fixed ### Fixed
- Fix template conflicts - Fix template conflicts
## [v2.0.2] - 2019-09-20 ## [v2.0.2] - 2019-09-20
### Fixed ### Fixed
- Fix template conflicts - Fix template conflicts
## [v2.0.1] - 2019-09-20 ## [v2.0.1] - 2019-09-20
### Fixed ### Fixed
- Fix template import - Fix template import
## [v2.0.0] - 2019-09-19 ## [v2.0.0] - 2019-09-19
### Changed ### Changed
- Refactor Router and Schema - Refactor Router and Schema
## [v1.3.2] - 2019-08-15 ## [v1.3.2] - 2019-08-15
### Added ### Added
- ASGI 3 support - ASGI 3 support
- CI tests for python 3.8-dev - CI tests for python 3.8-dev
- Now requests have `state` a mapping object - Now requests have `state` a mapping object
### Deprecated ### Deprecated
- ASGI 2 - ASGI 2
## [v1.3.1] - 2019-04-28 ## [v1.3.1] - 2019-04-28
### Added ### Added
- Route params Converters - Route params Converters
- Add search for documentation pages - Add search for documentation pages
### Changed ### Changed
- Bump dependencies - Bump dependencies
## [v1.3.0] - 2019-02-22 ## [v1.3.0] - 2019-02-22
### Fixed ### Fixed
- Versioning issue - Versioning issue
- Multiple cookies. - Multiple cookies.
- Whitenoise returns not found. - Whitenoise returns not found.
- Other bugfixes. - Other bugfixes.
### Added ### Added
- Stream support via `resp.stream`. - Stream support via `resp.stream`.
- Cookie directives via `resp.set_cookie`. - Cookie directives via `resp.set_cookie`.
- Add `resp.html` to send HTML. - Add `resp.html` to send HTML.
- Other improvements. - Other improvements.
## [v1.1.3] - 2019-01-12 ## [v1.1.3] - 2019-01-12
### Changed ### Changed
- Refactor `_route_for` - Refactor `_route_for`
### Fixed ### Fixed
- Resolve startup/shutdwown events - Resolve startup/shutdwown events
## [v1.2.0] - 2018-12-29 ## [v1.2.0] - 2018-12-29
### Added ### Added
- Documentations - Documentations
### Changed ### Changed
- Use Starlette's LifeSpan middleware - Use Starlette's LifeSpan middleware
- Update denpendencies - Update denpendencies
### Fixed ### Fixed
- Fix route.is_class_based - Fix route.is_class_based
- Fix test_500 - Fix test_500
- Typos - Typos
## [v1.1.2] - 2018-11-11 ## [v1.1.2] - 2018-11-11
### Fixed ### Fixed
- Minor fixes for Open API - Minor fixes for Open API
- Typos - Typos
## [v1.1.1] - 2018-10-29 ## [v1.1.1] - 2018-10-29
### Changed ### Changed
- Run sync views in a threadpoolexecutor. - Run sync views in a threadpoolexecutor.
## [v1.1.0] - 2018-10-27 ## [v1.1.0] - 2018-10-27
### Added ### Added
- Support for `before_request`. - Support for `before_request`.
## [v1.0.5]- 2018-10-27 ## [v1.0.5]- 2018-10-27
### Fixed ### Fixed
- Fix sessions. - Fix sessions.
## [v1.0.4] - 2018-10-27 ## [v1.0.4] - 2018-10-27
### Fixed ### Fixed
- Potential bufix for cookies. - Potential bufix for cookies.
## [v1.0.3] - 2018-10-27 ## [v1.0.3] - 2018-10-27
### Fixed ### Fixed
- Bugfix for redirects. - Bugfix for redirects.
## [v1.0.2] - 2018-10-27 ## [v1.0.2] - 2018-10-27
### Changed ### Changed
- Improvement for static file hosting. - Improvement for static file hosting.
## [v1.0.1] - 2018-10-26 ## [v1.0.1] - 2018-10-26
### Changed ### Changed
- Improve cors configuration settings. - Improve cors configuration settings.
## [v1.0.0] - 2018-10-26 ## [v1.0.0] - 2018-10-26
### Changed ### Changed
- Move GraphQL support into a built-in plugin. - Move GraphQL support into a built-in plugin.
## [v0.3.3] - 2018-10-25 ## [v0.3.3] - 2018-10-25
### Added ### Added
- CORS support - CORS support
### Changed ### Changed
- Improved exceptions. - Improved exceptions.
## [v0.3.2] - 2018-10-25 ## [v0.3.2] - 2018-10-25
### Changed ### Changed
- Subtle improvements. - Subtle improvements.
## [v0.3.1] - 2018-10-24 ## [v0.3.1] - 2018-10-24
### Fixed ### Fixed
- Packaging fix. - Packaging fix.
## [v0.3.0] - 2018-10-24 ## [v0.3.0] - 2018-10-24
### Changed ### Changed
- Interactive Documentation endpoint. - Interactive Documentation endpoint.
- Minor improvements. - Minor improvements.
## [v0.2.3] - 2018-10-24 ## [v0.2.3] - 2018-10-24
### Changed ### Changed
- Overall improvements. - Overall improvements.
## [v0.2.2] - 2018-10-23 ## [v0.2.2] - 2018-10-23
### Added ### Added
- Show traceback info when background tasks raise exceptions. - Show traceback info when background tasks raise exceptions.
## [v0.2.1] - 2018-10-23 ## [v0.2.1] - 2018-10-23
### Added ### Added
- api.requests. - api.requests.
## [v0.2.0] - 2018-10-22 ## [v0.2.0] - 2018-10-22
### Added ### Added
- WebSocket support. - WebSocket support.
## [v0.1.6] - 2018-10-20 ## [v0.1.6] - 2018-10-20
### Added ### Added
- 500 support. - 500 support.
## [v0.1.5] - 2018-10-20 ## [v0.1.5] - 2018-10-20
### Added ### Added
- File upload support - File upload support
### Changed ### Changed
- Improvements to sequential media reading. - Improvements to sequential media reading.
## [v0.1.4] - 2018-10-19 ## [v0.1.4] - 2018-10-19
### Fixed ### Fixed
- Stability. - Stability.
## [v0.1.3] - 2018-10-18 ## [v0.1.3] - 2018-10-18
### Added ### Added
- Sessions support. - Sessions support.
## [v0.1.2] - 2018-10-18 ## [v0.1.2] - 2018-10-18
### Added ### Added
- Cookies support. - Cookies support.
## [v0.1.1] - 2018-10-17 ## [v0.1.1] - 2018-10-17
### Changed ### Changed
- Default routes. - Default routes.
## [v0.1.0] - 2018-10-17 ## [v0.1.0] - 2018-10-17
### Added ### Added
- Prototype of static application support. - Prototype of static application support.
## [v0.0.10] - 2018-10-17 ## [v0.0.10] - 2018-10-17
### Fixed ### Fixed
- Bugfix for async class-based views. - Bugfix for async class-based views.
## [v0.0.9] - 2018-10-17 ## [v0.0.9] - 2018-10-17
### Fixed ### Fixed
- Bugfix for async class-based views. - Bugfix for async class-based views.
## [v0.0.8] - 2018-10-17 ## [v0.0.8] - 2018-10-17
### Added ### Added
- GraphiQL Support. - GraphiQL Support.
### Changed ### Changed
- Improvement to route selection. - Improvement to route selection.
## [v0.0.7] - 2018-10-16 ## [v0.0.7] - 2018-10-16
### Changed ### Changed
- Immutable Request object. - Immutable Request object.
## [v0.0.6] - 2018-10-16 ## [v0.0.6] - 2018-10-16
### Added ### Added
- Ability to mount WSGI apps. - Ability to mount WSGI apps.
- Supply content-type when serving up the schema. - Supply content-type when serving up the schema.
## [v0.0.5] - 2018-10-15 ## [v0.0.5] - 2018-10-15
### Added ### Added
- OpenAPI Schema support. - OpenAPI Schema support.
- Safe load/dump yaml. - Safe load/dump yaml.
## [v0.0.4] - 2018-10-15 ## [v0.0.4] - 2018-10-15
### Added ### Added
- Asynchronous support for data uploads. - Asynchronous support for data uploads.
### Fixed ### Fixed
- Bug fixes. - Bug fixes.
## [v0.0.3] - 2018-10-13 ## [v0.0.3] - 2018-10-13
### Fixed ### Fixed
- Bug fixes. - Bug fixes.
## [v0.0.2] - 2018-10-13 ## [v0.0.2] - 2018-10-13
### Changed ### Changed
- Switch to ASGI/Starlette. - Switch to ASGI/Starlette.
## [v0.0.1] - 2018-10-12 ## [v0.0.1] - 2018-10-12
### Added ### Added
- Conception! - Conception!
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.0.0..HEAD [Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.4..HEAD
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0 [v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5 [v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4 [v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3 [v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2 [v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1 [v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0 [v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2 [v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1 [v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0 [v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0 [v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3 [v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2 [v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1 [v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0 [v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5 [v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4 [v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3 [v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2 [v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1 [v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0 [v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3 [v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2 [v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1 [v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0 [v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3 [v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2 [v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1 [v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0 [v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6 [v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5 [v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4 [v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3 [v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2 [v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1 [v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0 [v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10 [v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9 [v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8 [v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7 [v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6 [v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5 [v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4 [v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3 [v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
+10 -175
View File
@@ -1,178 +1,13 @@
Copyright 2018 Kenneth Reitz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Apache License http://www.apache.org/licenses/LICENSE-2.0
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
1. Definitions. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
"License" shall mean the terms and conditions for use, reproduction, limitations under the License.
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+1
View File
@@ -0,0 +1 @@
include LICENSE
+20
View File
@@ -0,0 +1,20 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = {editable = true, path = "."}
[dev-packages]
pytest = "*"
"flake8" = "*"
black = "*"
twine = "*"
flask = "*"
sphinx = "*"
marshmallow = "*"
pytest-cov = "*"
[pipenv]
allow_prereleases = true
Generated
+762
View File
@@ -0,0 +1,762 @@
{
"_meta": {
"hash": {
"sha256": "ea12c0d556a3ca0848b0eba291a11a5ea98a701f0885c2d030b2aeb1e5b9c15f"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"version": "==0.4.0"
},
"aniso8601": {
"hashes": [
"sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e",
"sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"
],
"version": "==7.0.0"
},
"apispec": {
"hashes": [
"sha256:5fdaa1173b32515cc83f9d413a49a6c37fafc2b87f6b40e95923d3e85f0942c5",
"sha256:9e88c51517a6515612e818459f61c1bc06c00f2313e5187828bdbabaa7461473"
],
"version": "==3.0.0"
},
"apistar": {
"hashes": [
"sha256:8da0d3f15748c8ed6e68914ba5b8f6dd5dff5afbe137950d07103575df0bce73"
],
"version": "==0.7.2"
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"graphene": {
"hashes": [
"sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896",
"sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"
],
"version": "==2.1.8"
},
"graphql-core": {
"hashes": [
"sha256:1488f2a5c2272dc9ba66e3042a6d1c30cea0db4c80bd1e911c6791ad6187d91b",
"sha256:da64c472d720da4537a2e8de8ba859210b62841bd47a9be65ca35177f62fe0e4"
],
"version": "==2.2.1"
},
"graphql-relay": {
"hashes": [
"sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335",
"sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5",
"sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6"
],
"version": "==2.0.0"
},
"graphql-server-core": {
"hashes": [
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
],
"version": "==1.1.1"
},
"h11": {
"hashes": [
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
],
"version": "==0.8.1"
},
"httptools": {
"hashes": [
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
],
"version": "==0.0.13"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
],
"version": "==2.10.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
],
"version": "==3.2.1"
},
"promise": {
"hashes": [
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"version": "==2.2.1"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"version": "==0.0.5"
},
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
],
"version": "==5.1.2"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"responder": {
"editable": true,
"path": "."
},
"rfc3986": {
"hashes": [
"sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405",
"sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"
],
"version": "==1.3.2"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
],
"version": "==1.6.1"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"starlette": {
"hashes": [
"sha256:e41ef52e711a82ef95c195674e5d8d41c75c6b1d6f5a275637eedd4cc2150a7f"
],
"version": "==0.12.10"
},
"typesystem": {
"hashes": [
"sha256:ba2bd10f1c5844d08dd8841e777bdee55bfca569bf21cb96cd0f91e0a4f66cd8"
],
"version": "==0.2.4"
},
"urllib3": {
"hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"version": "==1.25.6"
},
"uvicorn": {
"hashes": [
"sha256:8aa44f9d9c3082ef693950387ea25d376e32944df6d4071dbd8edc3c25a40c74"
],
"version": "==0.8.6"
},
"uvloop": {
"hashes": [
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
],
"version": "==0.12.2"
},
"websockets": {
"hashes": [
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
],
"version": "==7.0"
},
"whitenoise": {
"hashes": [
"sha256:22f79cf8f1f509639330f93886acaece8ec5ac5e9600c3b981d33c34e8a42dfd",
"sha256:6dfea214b7c12efd689007abf9afa87a426586e9dbc051873ad2c8e535e2a1ac"
],
"version": "==4.1.4"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.3.0"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"babel": {
"hashes": [
"sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
"sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
],
"version": "==2.7.0"
},
"black": {
"hashes": [
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==19.3b0"
},
"bleach": {
"hashes": [
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"version": "==3.1.0"
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"coverage": {
"hashes": [
"sha256:17a417c691de3fc88de027832267313e5ed2b2ea3956745b562c4c389e44d05b",
"sha256:24307e67ebd9dc06fcbab9b7fef87412a97746c1baabb04ed8a93d5c2ccfe5ba",
"sha256:2a5d44a9d8426bd3699123864e63f008dc8dea9df22d5216a141a25d4670f22c",
"sha256:3726b8f5461e103a40e380f52b4b4ccdf2eda55d5d72f037cee43627992b4462",
"sha256:39dd15bbc4880a64399e180925bbc21c0c316a3065f6455d2512039f5cb59b94",
"sha256:3bb121f5dd156aab4fba2ebad6b0ad605bc5dc305931140dc614b101aa9d81ed",
"sha256:3bfdea9226eaed97736c973a7d6d0bbf9e1c1f1c7391c8e9c2bb2d0dbae49156",
"sha256:43be906a16239c1aa9f3742e3e6b0a5dd24781a13ce401f063262e9b4e93b69f",
"sha256:4a54cac1b39b2925041a41bcd1f191898fe401618627d7c3abf127c32a1c6dd1",
"sha256:4e58d65b90d6f26b3ccca7cf0fe573ef847347b8734af596a087a21eebb681f5",
"sha256:50229727d9baf0cd7f5ee6b194bf9dea708e9a20823d93f9e04d710b0a60e757",
"sha256:5141cdb010e9cd6939e37b8c2769d535cb535d80ef94f927c8a306f2e05a4736",
"sha256:748ba2b950425b9aef9d1bde2d6af7023585505016bd634e578f76ada4a30465",
"sha256:75e635bc6730c88b04421b25a0afc47b9b80efc1ed57630839196eb475722e50",
"sha256:78556f51dbfb33f18794eee29a4a8542fd2e301aa0d072653930793974dced03",
"sha256:7de17133509210ecc256535bab2f9a5547f3016c44f984fe12b4c10d81a4623f",
"sha256:83bf376555898fe2dc50d111a34b0152b504e454ed1e13cdcda6e5d50ba0ed5b",
"sha256:87730b5e4c3a42674fe8f0ecbb0d556c59c7e12b11a65c2178f2787252a80dfd",
"sha256:9bb7819c020c20c6200764879f0b10b323d6d4719aa7b0ae316c9e35730f9e2d",
"sha256:9c825788acb13d49ac20455433f3b862029aa497e97faba8c998555a042a6b91",
"sha256:b2bb4941c8838fc9ea2fca3c52e6dd865d39bbbc014bde249161bf8fcccf2152",
"sha256:c1b44c6c680f137910cb0f5481a2ae9899787ca7019f110a3708d9e99df941be",
"sha256:c52c2bc67bd3ff8db685f7c5f03e34a95bddd58a535630161f28d1c485d61e22",
"sha256:d6845e46338695c571759be1c770b013c477111e785b26151ec9feb6cd063543",
"sha256:e292b32dfc80d9f271af2d52df95455248322156e764763c4bfb2385b2e33533"
],
"version": "==5.0a8"
},
"docutils": {
"hashes": [
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
],
"version": "==0.15.2"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"flake8": {
"hashes": [
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
],
"index": "pypi",
"version": "==3.7.8"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.1.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
"markers": "python_version < '3.8'",
"version": "==0.23"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
],
"version": "==2.10.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
],
"version": "==3.2.1"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
],
"version": "==7.2.0"
},
"packaging": {
"hashes": [
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
],
"version": "==19.2"
},
"pkginfo": {
"hashes": [
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
],
"version": "==1.5.0.1"
},
"pluggy": {
"hashes": [
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
],
"version": "==0.13.0"
},
"py": {
"hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
],
"version": "==1.8.0"
},
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==2.1.1"
},
"pygments": {
"hashes": [
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
],
"version": "==2.4.2"
},
"pyparsing": {
"hashes": [
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
],
"version": "==2.4.2"
},
"pytest": {
"hashes": [
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
],
"index": "pypi",
"version": "==5.2.1"
},
"pytest-cov": {
"hashes": [
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
],
"index": "pypi",
"version": "==2.8.1"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"readme-renderer": {
"hashes": [
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
],
"version": "==24.0"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
],
"version": "==2.0.0"
},
"sphinx": {
"hashes": [
"sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845",
"sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"
],
"index": "pypi",
"version": "==2.2.0"
},
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
],
"version": "==1.0.1"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
],
"version": "==1.0.1"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
],
"version": "==1.0.2"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
],
"version": "==1.0.2"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
],
"version": "==1.1.3"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"tqdm": {
"hashes": [
"sha256:abc25d0ce2397d070ef07d8c7e706aede7920da163c64997585d42d3537ece3d",
"sha256:dd3fcca8488bb1d416aa7469d2f277902f26260c45aa86b667b074cd44b3b115"
],
"version": "==4.36.1"
},
"twine": {
"hashes": [
"sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1",
"sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993"
],
"index": "pypi",
"version": "==2.0.0"
},
"urllib3": {
"hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"version": "==1.25.6"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"werkzeug": {
"hashes": [
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
],
"version": "==0.16.0"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
}
}
}
+44 -83
View File
@@ -1,109 +1,70 @@
# Responder # Responder: a familiar HTTP Service Framework for Python
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/). [![Build Status](https://travis-ci.org/taoufik07/responder.svg?branch=master)](https://travis-ci.org/taoufik07/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/taoufik07/responder.svg)](https://github.com/taoufik07/responder/graphs/contributors)
```python [![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
import responder
api = responder.API()
@api.route("/{greeting}") Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__": This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
api.run()
```
$ pip install responder
That's it. Supports Python 3.9+. ## Testimonials
## The Basics > "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
- `resp.text` sends back text. `resp.html` sends back HTML. `resp.content` sends back bytes. > "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
- `resp.media` sends back JSON (or YAML, with content negotiation).
- `resp.file("path.pdf")` serves a file with automatic content-type detection.
- `req.headers` is case-insensitive. `req.params` gives you query parameters.
- Both sync and async views work — the `async` is optional.
## Highlights > "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
```python
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
# HTTP method filtering ## More Examples
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
# Class-based views See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# Custom error handling # Installing Responder
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
# Lifespan events Install the stable release:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
api = responder.API(lifespan=lifespan) $ pipenv install responder
✨🍰✨
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
# WebSockets Or, install from the development branch:
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
# Mount WSGI/ASGI apps $ pipenv install -e git+https://github.com/taoufik07/responder.git#egg=responder
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
# Background tasks Only **Python 3.6+** is supported.
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
```
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: `str`, `int`, `float`, `uuid`, `path`. # The Basic Idea
## Documentation The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
https://responder.kennethreitz.org - Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## Ideas
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- A production static file server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
- Provide an official way to run webpack.
+48
View File
@@ -0,0 +1,48 @@
alabaster==0.7.12
appdirs==1.4.3
atomicwrites==1.2.1
attrs==18.2.0
babel==2.6.0
black==18.9b0
bleach==3.0.2
certifi==2018.8.24
cffi==1.11.5
chardet==3.0.4
click==7.0
cmarkgfm==0.4.2
colorama==0.4.0 ; sys_platform == 'win32'
docutils==0.14
flake8==3.5.0
flask==1.0.2
future==0.16.0
idna==2.7
imagesize==1.1.0
itsdangerous==0.24
jinja2==2.10
markupsafe==1.0
mccabe==0.6.1
more-itertools==4.3.0
packaging==18.0
pkginfo==1.4.2
pluggy==0.7.1
py==1.7.0
pycodestyle==2.3.1
pycparser==2.19
pyflakes==1.6.0
pygments==2.2.0
pyparsing==2.2.2
pytest==3.8.2
pytz==2018.5
readme-renderer==22.0
requests-toolbelt==0.8.0
requests==2.19.1
six==1.11.0
snowballstemmer==1.2.1
sphinx==1.8.1
sphinxcontrib-websupport==1.1.0
toml==0.10.0
tqdm==4.26.0
twine==1.12.1
urllib3==1.23
webencodings==0.5.1
werkzeug==0.15.5
+7
View File
@@ -0,0 +1,7 @@
/* Hide module name and default value for environment variable section */
div[id$='environment-variables'] code.descclassname {
display: none;
}
div[id$='environment-variables'] em.property {
display: none;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
<!-- sorry your browser is not supported. -->
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
/*
Copyright (C) 2011-2018 Hoefler & Co.
This software is the property of Hoefler & Co. (H&Co).
Your right to access and use this software is subject to the
applicable License Agreement, or Terms of Service, that exists
between you and H&Co. If no such agreement exists, you may not
access or use this software for any purpose.
This software may only be hosted at the locations specified in
the applicable License Agreement or Terms of Service, and only
for the purposes expressly set forth therein. You may not copy,
modify, convert, create derivative works from or distribute this
software in any way, or make it accessible to any third party,
without first obtaining the written permission of H&Co.
For more information, please visit us at http://typography.com.
148887-130097-20181011
*/
<!-- sorry your browser is not supported. -->
File diff suppressed because one or more lines are too long
+151
View File
@@ -0,0 +1,151 @@
/*
* Konami-JS ~
* :: Now with support for touch events and multiple instances for
* :: those situations that call for multiple easter eggs!
* Code: https://github.com/snaptortoise/konami-js
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
* Version: 1.6.2 (7/17/2018)
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
*/
var Konami = function (callback) {
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
}
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = '';
e.preventDefault();
return false;
}
},
load: function (link) {
this._currentLink = link;
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, 'keydown', this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
touchendHandler: function () {
konami.iphone.input.push(konami.iphone.check_direction());
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
if (konami.iphone.input.length === konami.iphone.keys.length) {
var match = true;
for (var i = 0; i < konami.iphone.keys.length; i++) {
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
match = false;
}
}
if (match) {
konami.iphone.code(konami._currentLink);
}
}
},
touchstartHandler: function (e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function (link) {
this.orig_keys = this.keys;
konami.addEvent(document, "touchmove", this.touchmoveHandler);
konami.addEvent(document, "touchend", this.touchendHandler, false);
konami.addEvent(document, "touchstart", this.touchstartHandler);
},
unload: function () {
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
konami.removeEvent(document, 'touchend', this.touchendHandler);
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
},
check_direction: function () {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
result = (x_magnitude > y_magnitude) ? x : y;
result = (this.tap === true) ? "TAP" : result;
return result;
}
}
}
typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") {
konami.code = callback;
konami.load();
}
return konami;
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Konami;
} else {
if (typeof define === 'function' && define.amd) {
define([], function() {
return Konami;
});
} else {
window.Konami = Konami;
}
}
+188 -3
View File
@@ -1,21 +1,206 @@
<link rel="stylesheet" type="text/css" href="https://cloud.typography.com/7584432/7586812/css/fonts.css" />
<script type="text/javascript">$('#searchbox').hide(0);</script>
<!--Alabaster (krTheme++) Hacks -->
<!-- CSS Adjustments (I'm very picky.) -->
<style type="text/css"> <style type="text/css">
/* Make the document a little wider. */ /* Rezzy requires precise alignment. */
img.logo {
margin-left: -20px !important;
}
h1 {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 600 !important;
}
.section {
font-family: "Mercury Text G1 A", "Mercury Text G1 B" !important;
font-style: normal !important;
font-weight: 400 !important;
}
pre,
.pre,
.class em,
.descname,
.method em {
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
font-weight: 400 !important;
}
.property {
color: lightgrey !important;
}
.method .descname {
color: #220a54;
}
.method {
margin-bottom: 2em;
}
.si,
.s2,
.s1,
.method em,
.class em {
font-style: italic !important;
color: grey;
}
.method em,
.class em {
margin-left: 0.3em;
margin-right: 0.3em;
}
.method p,
.class p {
font-family: "Mercury Text G1 A", "Mercury Text G1 B";
font-style: italic !important;
font-weight: 400 !important;
font-size: 1.15em;
}
.method p:first,
.class p:first {
background: #fffcbf;
}
.class .property {
display: none;
}
#testimonials p.attribution {
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
display: none;
}
/* Make the document a little wider, less code is cut-off. */
div.document { div.document {
width: 1008px; width: 1008px;
} }
/* Better spacing around code blocks. */ /* Much-improved spacing around code blocks. */
div.highlight pre { div.highlight pre {
padding: 11px 14px; padding: 11px 14px;
} }
/* Responsive layout. */ /* Remain Responsive! */
@media screen and (max-width: 1008px) { @media screen and (max-width: 1008px) {
div.sphinxsidebar { div.sphinxsidebar {
display: none; display: none;
} }
div.document { div.document {
width: 100% !important; width: 100% !important;
} }
/* Have code blocks escape the document right-margin. */
div.highlight pre {
margin-right: -30px;
}
} }
</style> </style>
<!-- Analytics tracking for Kenneth. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-127383416-1');
</script>
<!-- There are no more hacks. -->
<!-- இڿڰۣ-ڰۣ— -->
<!-- Love, Kenneth Reitz -->
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
<script>
var easter_egg = new Konami('https://www.myfortunecookie.co.uk/fortunes/' + (Math.floor(Math.random() * 152) + 1));
</script>
<style>
.injected {
display: none !important;
}
</style>
<!-- GitHub Logo -->
<a href="https://github.com/kennethreitz/responder" class="github-corner" aria-label="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor" class="octo-body"></path>
</svg>
</a>
<style>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0)
}
20%,
60% {
transform: rotate(-25deg)
}
40%,
80% {
transform: rotate(10deg)
}
}
@media (max-width:500px) {
.github-corner:hover .octo-arm {
animation: none
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
}
</style>
<!-- That was not a hack. That was art.
<!-- UserVoice JavaScript SDK (only needed once on a page) -->
<script>(function () { var uv = document.createElement('script'); uv.type = 'text/javascript'; uv.async = true; uv.src = '//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(uv, s) })()</script>
<!-- A tab to launch the Classic Widget -->
<script>
UserVoice = window.UserVoice || [];
UserVoice.push(['showTab', 'classic_widget', {
mode: 'feedback',
primary_color: '#fa8c28',
link_color: '#0a8cc6',
forum_id: 913660,
tab_label: 'Got feedback?',
tab_color: '#00994f',
tab_position: 'bottom-left',
tab_inverted: true
}]);
</script>
+46 -5
View File
@@ -1,14 +1,55 @@
<p class="logo"> <p class="logo">
<a href="{{ pathto(master_doc) }}"> <a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" /> <img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
</a> </a>
</p> </p>
<p> <p>
<strong>Responder</strong> — a familiar HTTP service framework for Python. <iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p> </p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
<style>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
@kennethreitz</a>
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
</p>
<h3>Useful Links</h3> <h3>Useful Links</h3>
<ul> <ul>
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li> <li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li> <li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul> </ul>
+55
View File
@@ -0,0 +1,55 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
</a>
</p>
<p>
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
<style>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
@kennethreitz</a>
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
-7
View File
@@ -1,7 +0,0 @@
# Backlog
## Future Ideas
- Consider adding `after_request` hooks (complement to `before_request`)
- Explore WebSocket before_request short-circuit support
- Add rate limiting middleware
- Consider async template rendering by default
-1
View File
@@ -1 +0,0 @@
../../CHANGELOG.md
-174
View File
@@ -1,174 +0,0 @@
Responder CLI
=============
Responder installs a command line program ``responder``. Use it to launch
a Responder application from a file or module, either located on a local
or remote filesystem, or object store.
Launch Module Entrypoint
------------------------
For loading a Responder application from a Python module, you will refer to
its ``API()`` instance using a `Python entry point object reference`_ that
points to a Python object. It is either in the form ``importable.module``,
or ``importable.module:object.attr``.
A basic invocation command to launch a Responder application:
.. code-block:: shell
responder run acme.app
The command above assumes a Python package ``acme`` including an ``app``
module ``acme/app.py`` that includes an attribute ``api`` that refers
to a ``responder.API`` instance, reflecting the typical layout of
a standard Responder application.
Loading a Responder application using an entrypoint specification will
inherit the capacities of `Python's import system`_, as implemented by
`importlib`_.
Launch Local File
-----------------
Acquire a minimal example single-file application, ``helloworld.py`` [1]_,
to your local filesystem, giving you the chance to edit it, and launch the
Responder HTTP service.
.. code-block:: shell
wget https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
responder run helloworld.py
.. note::
To validate the example application, invoke a HTTP request, for example using
`curl`_, `HTTPie`_, or your favourite browser at hand.
.. code-block:: shell
http http://127.0.0.1:5042/Hello
The response is no surprise.
::
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain
date: Sat, 26 Oct 2024 13:16:55 GMT
encoding: utf-8
server: uvicorn
Hello, world!
.. [1] The Responder application `helloworld.py`_ implements a basic echo handler.
Launch Remote File
------------------
You can also launch a single-file application where its Python file is stored
on a remote location.
Responder supports all filesystem adapters compatible with `fsspec`_, and
installs the adapters for Azure Blob Storage (az), Google Cloud Storage (gs),
GitHub, HTTP, and AWS S3 by default.
.. code-block:: shell
# Works 1:1.
responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
responder run github://kennethreitz:responder@/examples/helloworld.py
If you need access other kinds of remote targets, see the `list of
fsspec-supported filesystems and protocols`_. The next section enumerates
a few synthetic examples. The corresponding storage buckets do not even
exist, so don't expect those commands to work.
.. code-block:: shell
# Azure Blob Storage, Google Cloud Storage, and AWS S3.
responder run az://kennethreitz-assets/responder/examples/helloworld.py
responder run gs://kennethreitz-assets/responder/examples/helloworld.py
responder run s3://kennethreitz-assets/responder/examples/helloworld.py
# Hadoop Distributed File System (hdfs), SSH File Transfer Protocol (sftp),
# Common Internet File System (smb), Web-based Distributed Authoring and
# Versioning (webdav).
responder run hdfs://kennethreitz-assets/responder/examples/helloworld.py
responder run sftp://user@host/kennethreitz/responder/examples/helloworld.py
responder run smb://workgroup;user:password@server:port/responder/examples/helloworld.py
responder run webdav+https://user:password@server:port/responder/examples/helloworld.py
.. tip::
In order to install support for all filesystem types supported by fsspec, run:
.. code-block:: shell
uv pip install 'fsspec[full]'
When using ``uv``, this concludes within an acceptable time of approx.
25 seconds. If you need to be more selectively instead of using ``full``,
choose from one or multiple of the available `fsspec extras`_, which are:
abfs, arrow, dask, dropbox, fuse, gcs, git, github, hdfs, http, oci, s3,
sftp, smb, ssh.
Launch with Non-Standard Instance Name
--------------------------------------
By default, Responder will acquire an ``responder.API`` instance using the
symbol name ``api`` from the specified Python module.
If your main application file uses a different name than ``api``, please
append the designated symbol name to the launch target address.
It works like this for module entrypoints and local files:
.. code-block:: shell
responder run acme.app:service
responder run /path/to/acme/app.py:service
It works like this for URLs:
.. code-block:: shell
responder run http://app.server.local/path/to/acme/app.py#service
Within your ``app.py``, the instance would have been defined to use
the ``service`` symbol name instead of ``api``, like this:
.. code-block:: python
service = responder.API()
Build JavaScript Application
----------------------------
The ``build`` subcommand invokes ``npm run build``, optionally accepting
a target directory. By default, it uses the current working directory,
where it expects a regular NPM ``package.json`` file.
.. code-block:: shell
responder build
When specifying a target directory, Responder will change to that
directory beforehand.
.. code-block:: shell
responder build /path/to/project
.. _curl: https://curl.se/
.. _fsspec: https://filesystem-spec.readthedocs.io/en/latest/
.. _fsspec extras: https://github.com/fsspec/filesystem_spec/blob/2024.12.0/pyproject.toml#L27-L69
.. _helloworld.py: https://github.com/kennethreitz/responder/blob/main/examples/helloworld.py
.. _HTTPie: https://httpie.io/docs/cli
.. _importlib: https://docs.python.org/3/library/importlib.html
.. _list of fsspec-supported filesystems and protocols: https://github.com/fsspec/universal_pathlib#currently-supported-filesystems-and-protocols
.. _Python entry point object reference: https://packaging.python.org/en/latest/specifications/entry-points/
.. _Python's import system: https://docs.python.org/3/reference/import.html
+188 -18
View File
@@ -1,35 +1,103 @@
# Sphinx configuration for Responder documentation. # -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
import os # -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = "responder" project = "responder"
copyright = "2018-2026, Kenneth Reitz" copyright = "2018, A Kenneth Reitz project"
author = "Kenneth Reitz" author = "Kenneth Reitz"
# The short X.Y version
import os
# Path hackery to get current version number.
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
about = {} about = {}
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f: with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
exec(f.read(), about) exec(f.read(), about)
version = about["__version__"] version = about["__version__"]
# The full version, including alpha/beta/rc tags
release = about["__version__"] release = about["__version__"]
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.mathjax",
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"myst_parser", "sphinx.ext.githubpages",
"sphinx_copybutton",
"sphinx_design_elements",
] ]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
source_suffix = {".rst": "restructuredtext"}
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index" master_doc = "index"
language = "en"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [] exclude_patterns = []
# Theme # The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster" html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = { html_theme_options = {
"show_powered_by": False, "show_powered_by": False,
"github_user": "kennethreitz", "github_user": "kennethreitz",
@@ -37,16 +105,118 @@ html_theme_options = {
"github_banner": False, "github_banner": False,
"show_related": False, "show_related": False,
} }
html_static_path = ["_static"]
html_sidebars = { html_sidebars = {
"index": ["sidebarintro.html", "searchbox.html"], "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"],
"**": ["sidebarintro.html", "localtoc.html", "searchbox.html"], "**": [
"sidebarlogo.html",
"localtoc.html",
"relations.html",
"sourcelink.html",
"searchbox.html",
"hacks.html",
],
} }
# MyST # Add any paths that contain custom static files (such as style sheets) here,
myst_heading_anchors = 3 # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Copybutton # Custom sidebar templates, must be a dictionary that maps document names
copybutton_remove_prompts = True # to template names.
copybutton_prompt_text = r">>> |\.\.\. |\$ " #
copybutton_prompt_is_regexp = True # The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "responderdoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "responder.tex", "responder Documentation", "Kenneth Reitz", "manual")
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "responder", "responder Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"responder",
"responder Documentation",
author,
"responder",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"https://docs.python.org/": None}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
+40 -68
View File
@@ -1,86 +1,58 @@
Deployment Deploying Responder
========== ===================
Responder applications are standard ASGI apps. You can deploy them anywhere You can deploy Responder anywhere you can deploy a basic Python application.
you'd deploy a Python web service.
Docker Deployment
-----------------
Running Locally Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
---------------
The simplest way to run your application:: ``Dockerfile``::
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
EXPOSE 80
That's it!
Heroku Deployment
-----------------
The basics::
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out an ``api.py``::
# api.py
import responder import responder
api = responder.API() api = responder.API()
@api.route("/") @api.route("/")
def hello(req, resp): async def hello(req, resp):
resp.text = "hello, world!" resp.text = "hello, world!"
if __name__ == "__main__": if __name__ == "__main__":
api.run() api.run()
This starts a production uvicorn server on ``127.0.0.1:5042``. Write out a ``Procfile``::
web: python api.py
Docker That's it! Next, we commit and push to Heroku::
------
A minimal Dockerfile for deploying a Responder application:: $ git add -A
$ git commit -m 'initial commit'
FROM python:3.13-slim $ git push heroku master
WORKDIR /app
COPY . .
RUN pip install responder
ENV PORT=80
EXPOSE 80
CMD ["python", "api.py"]
Build and run::
$ docker build -t myapi .
$ docker run -p 8000:80 myapi
Cloud Platforms
---------------
Responder automatically honors the ``PORT`` environment variable, which is
set by most cloud platforms. When ``PORT`` is set, Responder binds to
``0.0.0.0`` on that port automatically.
This works out of the box with:
- **Fly.io**
- **Railway**
- **Render**
- **Google Cloud Run**
- **Azure Container Apps**
- **AWS App Runner**
Just deploy your code and set the start command to ``python api.py``.
Uvicorn Directly
----------------
For more control over the production server, you can bypass ``api.run()``
and use uvicorn directly::
$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
This gives you access to all of uvicorn's options: worker count, SSL
certificates, access logging, and more. See the
`uvicorn documentation <https://www.uvicorn.org/>`_ for details.
Reverse Proxy
-------------
In production, you may want to place Responder behind a reverse proxy like
nginx or Caddy for SSL termination, load balancing, or serving static assets.
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers.
+102 -71
View File
@@ -1,7 +1,25 @@
Responder .. responder documentation master file, created by
========= sphinx-quickstart on Thu Oct 11 12:58:34 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
A familiar HTTP Service Framework for Python. A familiar HTTP Service Framework
=================================
|Build Status| |image1| |image2| |image3| |image4| |image5|
.. |Build Status| image:: https://travis-ci.org/kennethreitz/responder.svg?branch=master
:target: https://travis-ci.org/kennethreitz/responder
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
:target: https://pypi.org/project/responder/
.. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
:target: https://pypi.org/project/responder/
.. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://github.com/kennethreitz/responder/graphs/contributors
.. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
.. code:: python .. code:: python
@@ -16,96 +34,109 @@ A familiar HTTP Service Framework for Python.
if __name__ == '__main__': if __name__ == '__main__':
api.run() api.run()
Powered by `Starlette`_ and `uvicorn`_. The ``async`` is optional. Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
This gets you a ASGI app, with a production static files server
(`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
pre-installed, jinja2 templating (without additional imports), and a
production webserver based on uvloop, serving up requests with
automatic gzip compression.
The Idea Features
-------- --------
Responder takes the best ideas from `Flask`_ and `Falcon`_ and brings them - A pleasant API, with a single import statement.
together into one clean framework. - Class-based views without inheritance.
- `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- `f-string syntax <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
- OpenAPI schema generation, with interactive documentation!
- Single-page webapp support!
The request and response objects are passed into every view and mutated Testimonials
directly — no return values, no boilerplate. If you've used Requests,
you'll feel right at home. If you've used Flask, the routing will look
familiar. If you've used Falcon, the ``req`` / ``resp`` pattern will
click immediately.
- ``resp.text`` sends back text. ``resp.html`` sends back HTML.
- ``resp.media`` sends back JSON — or YAML, if the client asks for it.
- ``resp.file("path")`` serves a file. ``resp.content`` sends raw bytes.
- ``req.headers`` is case-insensitive. ``req.params`` holds query parameters.
- ``resp.status_code``, ``req.method``, ``req.url`` — the usual suspects.
Content negotiation happens automatically. Set ``resp.media`` to a dict
and Responder figures out the rest.
Responder and `FastAPI`_ share DNA — both are built on Starlette, both
appeared around the same time, and both pushed Python's ASGI ecosystem
forward. FastAPI went deep on type annotations and automatic validation.
Responder went for a mutable request/response pattern and a simpler,
more familiar API. Both projects are better for the other existing, and
you should use whichever feels right for what you're building.
What You Get
------------ ------------
One ``pip install``, batteries included: “Pleasantly very taken with python-responder.
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
best.”
- Mount Flask, Django, or any WSGI/ASGI app at a subroute. —Rudraksh M.K.
- Gzip compression, HSTS, CORS, and trusted host validation.
- Before-request hooks that can short-circuit for auth guards.
- A test client for fast, in-process testing with pytest.
- Route parameters with f-string syntax and type convertors.
- Lifespan context managers for startup and shutdown logic.
- Custom exception handlers for clean error responses.
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
- File serving with automatic content-type detection.
- Sync and async views — ``async`` is always optional.
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
- A pleasant API with a single import statement.
- OpenAPI schema generation with Swagger UI.
- A production `uvicorn`_ server, ready to deploy.
- HTTP method filtering for REST APIs.
- Signed cookie-based sessions.
- Background tasks in a thread pool.
- WebSocket support.
Installation
------------
.. code-block:: shell ..
$ uv pip install responder "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that."
Python 3.9 and above. That's it. —Tom Christie, author of `Django REST Framework`_
..
“I love that you are exploring new patterns. Go go go!”
— Danny Greenfield, author of `Two Scoops of Django`_
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _Two Scoops of Django: https://www.twoscoopspress.com/products/two-scoops-of-django-1-11
User Guides
-----------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: User Guide
quickstart quickstart
tour tour
deployment deployment
testing testing
api api
cli
.. toctree::
:maxdepth: 1
:caption: Project
changes
Sandbox <sandbox>
backlog
.. _Starlette: https://www.starlette.io/ Installing Responder
.. _uvicorn: https://www.uvicorn.org/ --------------------
.. _Flask: https://flask.palletsprojects.com/
.. _Falcon: https://falconframework.org/ .. code-block:: shell
.. _FastAPI: https://fastapi.tiangolo.com/
.. _GraphQL: https://graphql.org/ $ pipenv install responder
✨🍰✨
Only **Python 3.6+** is supported.
The Basic Idea
--------------
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
- Setting ``resp.content`` sends back bytes.
- Setting ``resp.text`` sends back unicode, while setting ``resp.html`` sends back HTML.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.html``/``.content`` override this).
- Case-insensitive ``req.headers`` dict (from Requests directly).
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
Ideas
-----
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- `Uvicorn <https://www.uvicorn.org/>`_ is built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against `slowloris <https://en.wikipedia.org/wiki/Slowloris_(computer_security)>`_ attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+104 -161
View File
@@ -1,234 +1,177 @@
Quick Start Quick Start!
=========== ============
This guide will walk you through the basics of building a web service with This section of the documentation exists to provide an introduction to the Responder interface,
Responder. By the end, you'll know how to declare routes, handle requests, as well as educate the user on basic functionality.
send responses, render templates, and process background tasks.
Create a Web Service Declare a Web Service
-------------------- ---------------------
The first thing you need to do is declare a web service. This is the central The first thing you need to do is declare a web service::
object that holds all your routes, middleware, and configuration::
import responder import responder
api = responder.API() api = responder.API()
Hello World!
------------
Hello World Then, you can add a view / route to it.
-----------
Next, add a route. Here, we'll make the root URL say "hello, world!":: Here, we'll make the root URL say "hello world!"::
@api.route("/") @api.route("/")
def hello_world(req, resp): def hello_world(req, resp):
resp.text = "hello, world!" resp.text = "hello, world!"
Every view receives a ``req`` (request) and ``resp`` (response) object. You
don't need to return anything — just mutate the response directly.
Run the Server Run the Server
-------------- --------------
Start your web service with ``api.run()``:: Next, we can run our web service easily, with ``api.run()``::
api.run() api.run()
This spins up a production-grade uvicorn server on port ``5042``, ready for This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
incoming HTTP requests.
You can customize the port with ``api.run(port=8000)``. The ``PORT`` Note: you can pass ``port=5000`` if you want to customize the port. The ``PORT`` environment variable for established web service providers (e.g. Heroku) will automatically be honored and will set the listening address to ``0.0.0.0`` automatically (also configurable through the ``address`` keyword argument).
environment variable is also honored automatically — when set, Responder
binds to ``0.0.0.0`` on that port, which is what cloud platforms like
Fly.io, Railway, and Google Cloud Run expect.
.. note::
Both sync and async views are supported. The ``async`` keyword is always
optional — use it when you need to ``await`` something.
Route Parameters Accept Route Arguments
---------------- ----------------------
If you want dynamic URLs, use Python's familiar f-string syntax to declare If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
variables in your routes::
@api.route("/hello/{who}") @api.route("/hello/{who}")
def hello_to(req, resp, *, who): def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!" resp.text = f"hello, {who}!"
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``. A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
Route parameters are passed as keyword-only arguments (after the ``*``). Type convertors are also available::
Type Convertors
^^^^^^^^^^^^^^^
You can constrain route parameters to specific types. The parameter will be
automatically converted before it reaches your view::
@api.route("/add/{a:int}/{b:int}") @api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b): async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}" resp.text = f"{a} + {b} = {a + b}"
Supported types: Supported types: ``str``, ``int`` and ``float``.
- ``str`` — matches any string without slashes (default) Returning JSON / YAML
- ``int`` — matches digits, converts to ``int`` ---------------------
- ``float`` — matches decimal numbers, converts to ``float``
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
- ``path`` — matches any string *including* slashes, useful for file paths
If you want your API to send back JSON, simply set the ``resp.media`` property to a JSON-serializable Python object::
Sending Responses
-----------------
Responder gives you several ways to send data back to the client. Just set
the appropriate property on the response object.
**Text and HTML**::
resp.text = "plain text response"
resp.html = "<h1>HTML response</h1>"
**JSON** — the most common pattern for APIs. Set ``resp.media`` to any
JSON-serializable Python object::
@api.route("/hello/{who}/json") @api.route("/hello/{who}/json")
def hello_json(req, resp, *, who): def hello_to(req, resp, *, who):
resp.media = {"hello": who} resp.media = {"hello": who}
If the client sends an ``Accept: application/x-yaml`` header, the same data A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
will be returned as YAML instead. Content negotiation is automatic.
**Files** — serve a file from disk with automatic content-type detection:: If the client requests YAML instead (with a header of ``Accept: application/x-yaml``), YAML will be sent.
resp.file("reports/annual.pdf") Rendering a Template
--------------------
**Raw bytes**:: Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
resp.content = b"\x89PNG\r\n..." Usage::
**Status codes and headers**:: from responder.templates import Templates
resp.status_code = 201 templates = Templates()
resp.headers["X-Custom"] = "value"
**Redirects**:: @api.route("/hello/{name}/html")
def hello(req, resp, name):
api.redirect(resp, location="/new-url") resp.html = templates.render("hello.html", name=name)
Reading Requests Also a ``render_async`` is available::
----------------
The request object gives you access to everything the client sent. templates = Templates(enable_async=True)
resp.html = await templates.render_async("hello.html", who=who)
**Method and URL**:: You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
req.method # "get", "post", etc. (lowercase) @api.route("/hello/{who}/html")
req.full_url # "http://example.com/path?q=1" def hello_html(req, resp, *, who):
req.url # parsed URL object resp.html = api.template('hello.html', who=who)
**Headers** — case-insensitive, just like you'd expect::
req.headers["Content-Type"]
req.headers["content-type"] # same thing
**Query parameters**::
# GET /search?q=python&page=2
req.params["q"] # "python"
req.params["page"] # "2"
**Path parameters** — also available on the request object::
req.path_params["user_id"] # same as the keyword argument
**Request body** — for POST/PUT/PATCH requests, you need to ``await`` the
body content::
# JSON body
data = await req.media()
# Form data
data = await req.media("form")
# File uploads
files = await req.media("files")
# Raw bytes
body = await req.content
# Raw text
text = await req.text
**Other useful properties**::
req.is_json # True if content type is JSON
req.cookies # dict of cookies
req.session # session data (dict)
req.client # (host, port) tuple
req.is_secure # True if HTTPS
Rendering Templates Setting Response Status Code
------------------- ----------------------------
Responder includes built-in `Jinja2 <https://jinja.palletsprojects.com/>`_ If you want to set the response status code, simply set ``resp.status_code``::
support. Templates are loaded from the ``templates/`` directory by default.
The simplest way is to use ``api.template()``:: @api.route("/416")
def teapot(req, resp):
@api.route("/hello/{name}/html") resp.status_code = api.status_codes.HTTP_416 # ...or 416
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
You can also use the ``Templates`` class directly for more control::
from responder.templates import Templates
templates = Templates(directory="templates")
@api.route("/page")
def page(req, resp):
resp.html = templates.render("page.html", title="Hello")
Async rendering is supported too::
templates = Templates(directory="templates", enable_async=True)
resp.html = await templates.render_async("page.html", title="Hello")
You can render template strings without a file::
resp.html = api.template_string("Hello, {{ name }}!", name="world")
Background Tasks Setting Response Headers
---------------- ------------------------
Sometimes you want to accept a request, respond immediately, and do the If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
actual processing later. Responder makes this easy with background tasks::
@api.route("/pizza")
def pizza_pizza(req, resp):
resp.headers['X-Pizza'] = '42'
That's it!
Receiving Data & Background Tasks
---------------------------------
If you're expecting to read any request data, on the server, you need to declare your view as async and await the content.
Here, we'll process our data in the background, while responding immediately to the client::
import time
@api.route("/incoming") @api.route("/incoming")
async def receive_incoming(req, resp): async def receive_incoming(req, resp):
data = await req.media()
@api.background.task @api.background.task
def process_data(data): def process_data(data):
"""This runs in a background thread.""" """Just sleeps for three seconds, as a demo."""
import time time.sleep(3)
time.sleep(10) # simulate heavy work
# Parse the incoming data as form-encoded.
# Note: 'json' and 'yaml' formats are also automatically supported.
data = await req.media()
# Process the data (in the background).
process_data(data) process_data(data)
# Respond immediately — processing continues in the background # Immediately respond that upload was successful.
resp.media = {"status": "accepted"} resp.media = {'success': True}
The ``@api.background.task`` decorator wraps any function to run in a thread A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
pool. The client gets an immediate response while the work continues.
Here's a sample code to post a file with background::
@api.route("/")
async def upload_file(req, resp):
@api.background.task
def process_data(data):
f = open('./{}'.format(data['file']['filename']), 'w')
f.write(data['file']['content'].decode('utf-8'))
f.close()
data = await req.media(format='files')
process_data(data)
resp.media = {'success': 'ok'}
You can send a file easily with requests::
import requests
data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
r = requests.post('http://127.0.0.1:8210/file', files=data)
print(r.text)
-36
View File
@@ -1,36 +0,0 @@
(sandbox)=
# Development Sandbox
## Setup
Set up a development sandbox.
Acquire sources and create virtualenv.
```shell
git clone https://github.com/kennethreitz/responder.git
cd responder
uv venv
```
Install project in editable mode, including
all development tools.
```shell
uv pip install --upgrade --editable '.[develop,docs,release,test]'
```
## Operations
Run tests.
```shell
source .venv/bin/activate
pytest
```
Format code.
```shell
ruff format .
ruff check --fix .
```
Documentation authoring.
```shell
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
```
+41 -117
View File
@@ -1,46 +1,54 @@
Testing Building and Testing with Responder
======= ===================================
Responder includes a built-in test client powered by Starlette's Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
``TestClient``. You don't need to start a server — tests run in-process,
making them fast and reliable.
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
Getting Started The Basics
--------------- ----------
Given a simple application in ``api.py``:: Your repository should look like this::
Pipfile Pipfile.lock api.py test_api.py
``$ cat api.py``::
import responder import responder
api = responder.API() api = responder.API()
@api.route("/") @api.route("/")
def hello(req, resp): def hello_world(req, resp):
resp.text = "hello, world!" resp.text = "hello, world!"
if __name__ == "__main__": if __name__ == "__main__":
api.run() api.run()
You can test it with pytest::
# test_api.py ``$ cat Pipfile``::
import api as service
def test_hello(): [[source]]
r = service.api.requests.get("/") url = "https://pypi.org/simple"
assert r.text == "hello, world!" verify_ssl = true
name = "pypi"
Run your tests:: [packages]
responder = "*"
$ pytest [dev-packages]
pytest = "*"
[requires]
python_version = "3.7"
Using Fixtures [pipenv]
-------------- allow_prereleases = true
For larger test suites, use pytest fixtures to share the API instance Writing Tests
across tests:: -------------
``$ cat test_api.py``::
import pytest import pytest
import api as service import api as service
@@ -49,109 +57,25 @@ across tests::
def api(): def api():
return service.api return service.api
def test_hello(api):
def test_hello_world(api):
r = api.requests.get("/") r = api.requests.get("/")
assert r.text == "hello, world!" assert r.text == "hello, world!"
def test_json(api): ``$ pytest``::
@api.route("/data")
def data(req, resp):
resp.media = {"key": "value"}
r = api.requests.get(api.url_for(data)) ...
assert r.json() == {"key": "value"} ========================== 1 passed in 0.10 seconds ==========================
The ``api.url_for()`` method generates a URL for a given route endpoint,
so you don't have to hard-code paths in your tests.
Testing JSON APIs (Optional) Proper Python Package
----------------- --------------------------------
Send JSON data and check the response:: Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
def test_create_item(api): 1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
@api.route("/items") 2. ``$ pipenv install -e . --dev``
async def create(req, resp):
data = await req.media()
resp.media = {"created": data}
resp.status_code = 201
r = api.requests.post(api.url_for(create), json={"name": "widget"}) This will allow you to only specify your dependencies once: in ``setup.py``. ``$ pipenv lock`` will automatically lock your transitive dependencies (e.g. Responder), even if it's not specified in the ``Pipfile``.
assert r.status_code == 201
assert r.json() == {"created": {"name": "widget"}}
This will ensure that your application gets installed in every developer's environment, using Pipenv.
Testing File Uploads
--------------------
Send files using the ``files`` parameter::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"received": list(files.keys())}
files = {"doc": ("report.pdf", b"content", "application/pdf")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"received": ["doc"]}
Testing WebSockets
------------------
Use Starlette's ``TestClient`` directly for WebSocket connections::
from starlette.testclient import TestClient
def test_websocket(api):
@api.route("/ws", websocket=True)
async def ws(ws):
await ws.accept()
await ws.send_text("hello")
await ws.close()
client = TestClient(api)
with client.websocket_connect("/ws") as ws:
assert ws.receive_text() == "hello"
Testing Error Handling
----------------------
To test error responses without pytest raising the exception, disable
server exception propagation::
from starlette.testclient import TestClient
def test_500(api):
@api.route("/fail")
def fail(req, resp):
raise ValueError("something broke")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 500
Testing Lifespan Events
-----------------------
The test client supports lifespan events. Use ``with`` to ensure startup
and shutdown hooks run::
def test_with_lifespan(api):
started = {"value": False}
@api.on_event("startup")
async def on_startup():
started["value"] = True
@api.route("/")
def check(req, resp):
resp.media = {"started": started["value"]}
with api.requests as session:
r = session.get("http://;/")
assert r.json() == {"started": True}
+326 -407
View File
@@ -1,177 +1,41 @@
Feature Tour Feature Tour
============ ============
This section walks through Responder's features in detail. Each section
includes working code examples you can copy into your application.
Method Filtering
----------------
By default, a route matches all HTTP methods. If you want to restrict a
route to specific methods, pass the ``methods`` parameter::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
resp.media = {"items": []}
@api.route("/items", methods=["POST"], check_existing=False)
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
Note the ``check_existing=False`` — this allows you to register multiple
handlers for the same path with different methods.
Class-Based Views Class-Based Views
----------------- -----------------
For more complex resources, you can use class-based views. Responder will Class-based views (and setting some headers and stuff)::
dispatch to the appropriate method handler based on the HTTP method::
@api.route("/{greeting}") @api.route("/{greeting}")
class GreetingResource: class GreetingResource:
def on_get(self, req, resp, *, greeting): def on_request(self, req, resp, *, greeting): # or on_get...
resp.text = f"{greeting}, world!" resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
def on_post(self, req, resp, *, greeting): resp.status_code = api.status_codes.HTTP_416
resp.media = {"received": greeting}
def on_request(self, req, resp, *, greeting):
"""Called on EVERY request, before the method-specific handler."""
resp.headers["X-Greeting"] = greeting
The ``on_request`` method is called for all HTTP methods, much like
middleware scoped to a single route. Method-specific handlers (``on_get``,
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
No inheritance required — just define a class with the right method names.
Lifespan Events Background Tasks
--------------- ----------------
Modern applications often need to set up resources on startup (database Here, you can spawn off a background thread to run any function, out-of-request::
connections, caches, ML models) and tear them down on shutdown. Responder
supports the lifespan context manager pattern::
from contextlib import asynccontextmanager @api.route("/")
def hello(req, resp):
@asynccontextmanager @api.background.task
async def lifespan(app): def sleep(s=10):
# Startup — runs before the first request time.sleep(s)
print("connecting to database...") print("slept!")
yield
# Shutdown — runs after the server stops
print("closing connections...")
api = responder.API(lifespan=lifespan) sleep()
resp.content = "processing"
You can also use the traditional event decorator style::
@api.on_event("startup")
async def startup():
print("starting up")
@api.on_event("shutdown")
async def shutdown():
print("shutting down")
The context manager approach is preferred for new code — it makes the
startup/shutdown relationship explicit and keeps related code together.
Serving Files
-------------
Serve files from disk with automatic content-type detection. Responder
uses Python's ``mimetypes`` module to figure out the right ``Content-Type``
header for you::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can override the content type if needed::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
Custom Error Handling
---------------------
By default, unhandled exceptions result in a 500 Internal Server Error.
You can register custom handlers for specific exception types to return
structured error responses::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Now, any route that raises a ``ValueError`` will return a clean 400 response
with a JSON error message instead of a generic 500 page.
Before-Request Hooks
--------------------
Run code before every request. This is useful for logging, adding common
headers, or setting up per-request state::
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.1"
**Short-circuiting:** If your hook sets ``resp.status_code``, the route
handler will be skipped entirely and the response will be sent immediately.
This is the pattern for authentication guards::
@api.route(before_request=True)
def auth_check(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
If the ``Authorization`` header is missing, the client gets a 401 response
and the actual route handler never runs.
WebSocket hooks work the same way::
@api.before_request(websocket=True)
async def ws_auth(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets for real-time, bidirectional communication::
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
You can send and receive in multiple formats:
- ``send_text`` / ``receive_text`` — plain text
- ``send_json`` / ``receive_json`` — JSON objects
- ``send_bytes`` / ``receive_bytes`` — raw binary data
GraphQL GraphQL
------- -------
Responder includes built-in GraphQL support via Serve a GraphQL API::
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
with a single method call::
import graphene import graphene
@@ -181,328 +45,383 @@ with a single method call::
def resolve_hello(self, info, name): def resolve_hello(self, info, name):
return f"Hello {name}" return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query)) schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
Visiting ``/graphql`` in a browser renders the GraphiQL interactive IDE, api.add_route("/graph", view)
where you can explore your schema and test queries. Programmatic clients
can POST JSON queries to the same endpoint.
You can access the Responder request and response objects in your resolvers Visiting the endpoint will render a *GraphiQL* instance, in the browser.
through ``info.context["request"]`` and ``info.context["response"]``.
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
OpenAPI Documentation OpenAPI Schema Support
--------------------- ----------------------
Responder can generate an OpenAPI schema and serve interactive API Responder comes with built-in support for OpenAPI / marshmallow
documentation automatically::
api = responder.API( New in Responder `1.4.0`::
title="Pet Store",
import responder
from responder.ext.schema import Schema as OpenAPISchema
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API()
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0", version="1.0",
openapi="3.0.2", openapi="3.0.2",
docs_route="/docs", description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
) )
This gives you: @schema.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
- An OpenAPI schema at ``/schema.yml``
- Interactive Swagger UI documentation at ``/docs``
There are three ways to document your endpoints. @api.route("/")
def route(req, resp):
**Pydantic models** — the recommended approach for new APIs. Use """A cute furry animal endpoint.
``request_model`` and ``response_model`` to annotate your routes, and
Responder will generate the schema automatically::
from pydantic import BaseModel
class PetIn(BaseModel):
name: str
age: int = 0
class PetOut(BaseModel):
id: int
name: str
age: int
@api.route("/pets", methods=["POST"],
request_model=PetIn, response_model=PetOut)
async def create_pet(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
This generates a full OpenAPI path with ``requestBody`` and ``responses``
schemas, all linked by ``$ref`` to your Pydantic models in
``components/schemas``.
You can also register standalone schemas with the ``@api.schema`` decorator::
@api.schema("Pet")
class Pet(BaseModel):
name: str
age: int = 0
**YAML docstrings** — inline your OpenAPI spec directly in the docstring.
This gives you full control over every detail::
@api.route("/pets")
def list_pets(req, resp):
"""A list of pets.
--- ---
get: get:
description: Get all pets description: Get a random pet
responses: responses:
200: 200:
description: A list of pets description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
""" """
resp.media = [{"name": "Fido"}] resp.media = PetSchema().dump({"name": "little orange"})
**Marshmallow schemas** — if you're already using marshmallow for
validation, Responder integrates with it via the apispec plugin::
Old way *It's recommended to use the code above* ::
import responder
from marshmallow import Schema, fields from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@api.schema("Pet") @api.schema("Pet")
class PetSchema(Schema): class PetSchema(Schema):
name = fields.Str() name = fields.Str()
All three approaches can be mixed in the same API. Pydantic models, @api.route("/")
marshmallow schemas, and YAML docstrings all contribute to the same def route(req, resp):
generated OpenAPI specification. """A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
You can choose from multiple documentation themes: ::
``swagger_ui`` (default), ``redoc``, ``rapidoc``, or ``elements``.
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
responses: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
securitySchemes: {}
info:
contact: {email: support@example.com, name: API Support, url: 'http://www.example.com/support'}
description: This is a sample server for a pet store.
license: {name: Apache 2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0.html'}
termsOfService: http://example.com/terms/
title: Web Service
version: 1.0
openapi: 3.0.2
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref: "#/components/schemas/Pet"}
tags: []
Mounting Other Apps Interactive Documentation
------------------- -------------------------
Responder can mount any WSGI or ASGI application at a subroute. This means Responder can automatically supply API Documentation for you. Using the example above
you can gradually migrate from Flask, or run multiple frameworks side by side::
from flask import Flask The new and recommended way::
flask_app = Flask(__name__) ...
from responder.ext.schema import Schema
...
api = responder.API()
@flask_app.route("/") schema = Schema(
def hello(): app=api,
return "Hello from Flask!" title="Web Service",
version="1.0",
openapi="3.0.2",
...
docs_route='/docs',
...
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
api.mount("/flask", flask_app) The old way ::
Requests to ``/flask/`` will be handled by Flask. Everything else goes api = responder.API(
through Responder. Both WSGI and ASGI apps are supported — Responder title="Web Service",
wraps WSGI apps automatically. version="1.0",
openapi="3.0.2",
docs_route='/docs',
Cookies description=description,
------- terms_of_service=terms_of_service,
contact=contact,
Reading and writing cookies is straightforward:: license=license,
# Read cookies from the request
session_id = req.cookies.get("session_id")
# Set a cookie on the response
resp.cookies["hello"] = "world"
For more control over cookie directives, use ``set_cookie``::
resp.set_cookie(
"token",
value="abc123",
max_age=3600,
secure=True,
httponly=True,
path="/",
) )
Supported directives: ``key``, ``value``, ``expires``, ``max_age``, This will make ``/docs`` render interactive documentation for your API.
``domain``, ``path``, ``secure``, ``httponly``.
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
----------------------------------------------------
Cookie-Based Sessions Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
---------------------
Responder has built-in support for signed, cookie-based sessions. Just import responder
read from and write to the ``session`` dictionary:: from flask import Flask
@api.route("/login") api = responder.API()
def login(req, resp): flask = Flask(__name__)
resp.session["username"] = "alice"
@api.route("/profile") @flask.route('/')
def profile(req, resp): def hello():
resp.media = {"user": req.session.get("username")} return 'hello'
The session data is stored in a cookie called ``Responder-Session``. It's api.mount('/flask', flask)
signed for tamper protection, so you can trust that the data originated
from your server.
.. warning:: That's it!
For production use, always set a secret key:: Single-Page Web Apps
--------------------
api = responder.API(secret_key="your-secret-key-here") If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
Static Files
------------
Static files are served from the ``static/`` directory by default::
api = responder.API(static_dir="static", static_route="/static")
Place your CSS, JavaScript, images, and other assets in the ``static/``
directory and they'll be served automatically.
For single-page applications, you can serve ``index.html`` as the default
response for all unmatched routes::
api.add_route("/", static=True) api.add_route("/", static=True)
You can add additional static directories at runtime:: This will make ``index.html`` the default response to all undefined routes.
api.static_app.add_directory("extra_assets") Reading / Writing Cookies
-------------------------
Responder makes it very easy to interact with cookies from a Request, or add some to a Response::
>>> resp.cookies["hello"] = "world"
>>> req.cookies
{"hello": "world"}
To set cookies directives, you should use `resp.set_cookie`::
>>> resp.set_cookie("hello", value="world", max_age=60)
Supported directives:
* ``key`` - **Required**
* ``value`` - [OPTIONAL] - Defaults to ``""``.
* ``expires`` - Defaults to ``None``.
* ``max_age`` - Defaults to ``None``.
* ``domain`` - Defaults to ``None``.
* ``path`` - Defaults to ``"/"``.
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives>`_
Using Cookie-Based Sessions
---------------------------
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to the ``resp.session`` dictionary::
>>> resp.session['username'] = 'kennethreitz'
A cookie called ``Responder-Session`` will be set, which contains all the data in ``resp.session``. It is signed, for verification purposes.
You can easily read a Request's session data, that can be trusted to have originated from the API::
>>> req.session
{'username': 'kennethreitz'}
**Note**: if you are using this in production, you should pass the ``secret_key`` argument to ``API(...)``::
api = responder.API(secret_key=os.environ['SECRET_KEY'])
Using ``before_request``
------------------------
If you'd like a view to be executed before every request, simply do the following::
@api.route(before_request=True)
def prepare_response(req, resp):
resp.headers["X-Pizza"] = "42"
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
For ``websockets``::
@api.route(before_request=True, websocket=True)
def prepare_response(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets::
@api.route('/ws', websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
Accepting the connection::
await websocket.accept()
Sending and receiving data::
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
Supported formats: ``text``, ``json``, ``bytes``.
Closing the connection::
await websocket.close()
Using Requests Test Client
--------------------------
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
Here's an example of a test (written with pytest)::
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
HSTS (Redirect to HTTPS)
------------------------
Want HSTS (to redirect all traffic to HTTPS)?
::
api = responder.API(enable_hsts=True)
Boom.
CORS CORS
---- ----
Enable Cross-Origin Resource Sharing for your API:: Want `CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/>`_ ?
api = responder.API(cors=True, cors_params={ ::
"allow_origins": ["https://example.com"],
"allow_methods": ["GET", "POST"],
"allow_headers": ["*"],
"allow_credentials": True,
"max_age": 600,
})
The default CORS policy is restrictive — you must explicitly enable the api = responder.API(cors=True)
origins, methods, and headers your frontend needs.
HSTS The default parameters used by **Responder** are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
----
Force all traffic to HTTPS with a single flag:: In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
api = responder.API(enable_hsts=True)
This adds the ``Strict-Transport-Security`` header and redirects HTTP
requests to HTTPS.
* ``allow_origins`` - A list of origins that should be permitted to make cross-origin requests. eg. ``['https://example.org', 'https://www.example.org']``. You can use ``['*']`` to allow any origin.
* ``allow_origin_regex`` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. ``'https://.*\.example\.org'``.
* ``allow_methods`` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use ``['*']`` to allow all standard methods.
* ``allow_headers`` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to ``[]``. You can use ``['*']`` to allow all headers. The ``Accept``, ``Accept-Language``, ``Content-Language`` and ``Content-Type`` headers are always allowed for CORS requests.
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
Trusted Hosts Trusted Hosts
------------- -------------
Protect against HTTP Host header attacks by restricting which hostnames Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
your application will respond to::
api = responder.API(allowed_hosts=["example.com", "*.example.com"]) A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
Requests with a ``Host`` header that doesn't match any of the patterns ::
will receive a 400 Bad Request response. Wildcard domains are supported.
By default, all hostnames are allowed. api = responder.API(allowed_hosts=['example.com', 'tenant.example.com'])
* ``allowed_hosts`` - A list of allowed hostnames.
Server-Sent Events (SSE) Note:
------------------------
Stream real-time updates to the client using Server-Sent Events. This is * By default, all hostnames are allowed.
great for live feeds, progress updates, and AI streaming responses:: * Wildcard domains such as ``*.example.com`` are supported.
* To allow any hostname use ``allowed_hosts=["*"]``.
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
Each yielded value can be a string (treated as data) or a dict with
``data``, ``event``, ``id``, and ``retry`` fields::
yield {"event": "update", "data": "hello", "id": "1"}
yield "simple string message"
Streaming Files
---------------
For large files, use ``resp.stream_file()`` to stream the content without
loading the entire file into memory::
@api.route("/download")
def download(req, resp):
resp.stream_file("large-dataset.csv")
For small files where memory isn't a concern, ``resp.file()`` loads the
entire file at once — simpler but less efficient for large files.
After-Request Hooks
-------------------
Run code after every request, useful for logging, adding headers, or
cleanup::
@api.after_request()
def log_response(req, resp):
print(f"{req.method} {req.full_url} -> {resp.status_code}")
Route Groups
------------
Organize related routes with a shared URL prefix. Useful for API versioning
and logical grouping::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Request ID
----------
Auto-generate unique request IDs for tracing and debugging. If the client
sends an ``X-Request-ID`` header, it's forwarded; otherwise a new UUID is
generated::
api = responder.API(request_id=True)
Rate Limiting
-------------
Built-in token bucket rate limiter::
from responder.ext.ratelimit import RateLimiter
limiter = RateLimiter(requests=100, period=60) # 100 req/min
limiter.install(api)
When the limit is exceeded, clients receive a ``429 Too Many Requests``
response with ``Retry-After`` and ``X-RateLimit-Remaining`` headers.
MessagePack
-----------
In addition to JSON and YAML, Responder supports MessagePack for efficient
binary serialization::
# Decode MessagePack request body
data = await req.media("msgpack")
# Content negotiation also works — clients can send
# Accept: application/x-msgpack to receive MessagePack responses.
-19
View File
@@ -1,19 +0,0 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "hello, world!"
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
-26
View File
@@ -1,26 +0,0 @@
# Example showing the lifespan context manager pattern.
# https://pypi.org/project/responder/
from contextlib import asynccontextmanager
import responder
@asynccontextmanager
async def lifespan(app):
# Startup: initialize resources
print("Starting up...")
yield
# Shutdown: clean up resources
print("Shutting down...")
api = responder.API(lifespan=lifespan)
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
-25
View File
@@ -1,25 +0,0 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "Welcome"
@api.route("/user")
async def user_create(req, resp):
data = await req.media()
resp.text = f"Hello, {data['username']}"
@api.route("/user/{identifier}")
async def user_get(req, resp, *, identifier):
resp.text = f"Hello, user {identifier}"
if __name__ == "__main__":
api.run()
-184
View File
@@ -1,184 +0,0 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=42",
]
[project]
name = "responder"
description = "A familiar HTTP Service Framework for Python."
readme = "README.md"
license = {text = "Apache 2.0"}
authors = [
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
]
requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Internet :: WWW/HTTP",
]
dynamic = ["version"]
dependencies = [
"a2wsgi",
"apispec>=1.0.0",
"chardet",
"docopt-ng",
"graphene>=3",
"graphql-core>=3.1",
"marshmallow",
"msgpack",
"pueblo[sfa-full]>=0.0.11",
"pydantic>=2",
"python-multipart",
"starlette[full]>=0.40",
"uvicorn[standard]",
]
[project.optional-dependencies]
develop = [
"pyproject-fmt",
"ruff",
"validate-pyproject",
]
docs = [
"alabaster<1.1",
"myst-parser",
"sphinx>=5,<9",
"sphinx-autobuild",
"sphinx-copybutton",
"sphinx-design-elements",
]
release = ["build", "twine"]
test = [
"flask",
"mypy",
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-rerunfailures",
]
[project.scripts]
responder = "responder.ext.cli:cli"
[project.urls]
Homepage = "https://github.com/kennethreitz/responder"
Documentation = "https://responder.kennethreitz.org"
Repository = "https://github.com/kennethreitz/responder"
Issues = "https://github.com/kennethreitz/responder/issues"
[tool.setuptools.dynamic]
version = {attr = "responder.__version__.__version__"}
[tool.setuptools.package-data]
responder = ["py.typed"]
[tool.setuptools.packages.find]
exclude = ["tests"]
[tool.ruff]
line-length = 90
extend-exclude = [
"docs/source/conf.py",
]
lint.select = [
# Builtins
"A",
# Bugbear
"B",
# comprehensions
"C4",
# Pycodestyle
"E",
# eradicate
"ERA",
# Pyflakes
"F",
# isort
"I",
# pandas-vet
"PD",
# return
"RET",
# Bandit
"S",
# print
"T20",
"W",
# flake8-2020
"YTT",
]
lint.extend-ignore = [
"S101", # Allow use of `assert`.
]
lint.per-file-ignores."responder/util/cmd.py" = [ "A005" ] # Module shadows a Python standard-library module
lint.per-file-ignores."tests/*" = [
"ERA001", # Found commented-out code.
"S101", # Allow use of `assert`, and `print`.
]
[tool.pytest.ini_options]
addopts = """
-rfEXs -p pytester --strict-markers --verbosity=3
--cov --cov-report=term-missing --cov-report=xml
"""
filterwarnings = [
"error::UserWarning",
]
log_level = "DEBUG"
log_cli_level = "DEBUG"
log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
minversion = "2.0"
testpaths = [
"responder",
"tests",
]
markers = [
]
xfail_strict = true
[tool.coverage.run]
branch = false
omit = [
"*.html",
"tests/*",
]
[tool.coverage.report]
fail_under = 0
show_missing = true
exclude_lines = [
"# pragma: no cover",
"raise NotImplemented",
]
[tool.mypy]
packages = [
"responder",
]
exclude = [
]
check_untyped_defs = true
explicit_package_bases = true
ignore_missing_imports = true
implicit_optional = true
install_types = true
namespace_packages = true
non_interactive = true
+4
View File
@@ -0,0 +1,4 @@
[pytest]
;addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
+5
View File
@@ -0,0 +1,5 @@
build:
image: latest
python:
version: 3.6
+1 -17
View File
@@ -1,18 +1,2 @@
""" from .core import *
Responder - a familiar HTTP Service Framework.
This module exports the core functionality of the Responder framework,
including the API, Request, and Response classes.
"""
from . import ext from . import ext
from .__version__ import __version__
from .core import API, Request, Response
__all__ = [
"API",
"Request",
"Response",
"__version__",
"ext",
]
+4
View File
@@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.2.0" __version__ = "2.0.4"
+75 -254
View File
@@ -1,37 +1,41 @@
import asyncio import json
import os import os
from pathlib import Path from pathlib import Path
__all__ = ["API"] import jinja2
import uvicorn import uvicorn
from starlette.middleware.cors import CORSMiddleware from starlette.exceptions import ExceptionMiddleware
from starlette.middleware.wsgi import WSGIMiddleware
from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.gzip import GZipMiddleware from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Lifespan
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
from . import status_codes from . import models, status_codes
from .background import BackgroundQueue from .background import BackgroundQueue
from .formats import get_formats from .formats import get_formats
from .models import Request, Response
from .routes import Router from .routes import Router
from .statics import DEFAULT_API_THEME, DEFAULT_CORS_PARAMS, DEFAULT_SECRET_KEY
from .ext.schema import Schema as OpenAPISchema
from .staticfiles import StaticFiles from .staticfiles import StaticFiles
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
from .templates import Templates from .templates import Templates
class API: class API:
"""The primary web-service class. """The primary web-service class.
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist. :param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist. :param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped. :param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs. :param enable_hsts: If ``True``, send all responses to HTTPS URLs.
:param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui`` """
""" # noqa: E501
status_codes = status_codes status_codes = status_codes
@@ -40,12 +44,13 @@ class API:
*, *,
debug=False, debug=False,
title=None, title=None,
version=None, version="1.0",
description=None, description=None,
terms_of_service=None, terms_of_service=None,
contact=None, contact=None,
license=None, # noqa: A002 license=None,
openapi=None, openapi=None,
openapi_version="3.0.2",
openapi_route="/schema.yml", openapi_route="/schema.yml",
static_dir="static", static_dir="static",
static_route="/static", static_route="/static",
@@ -57,20 +62,17 @@ class API:
cors=False, cors=False,
cors_params=DEFAULT_CORS_PARAMS, cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None, allowed_hosts=None,
openapi_theme=DEFAULT_OPENAPI_THEME,
lifespan=None,
request_id=False,
): ):
self.background = BackgroundQueue() self.background = BackgroundQueue()
self.secret_key = secret_key self.secret_key = secret_key
self.router = Router(lifespan=lifespan) self.router = Router()
if static_dir is not None: if static_dir is not None:
if static_route is None: if static_route is None:
static_route = "" static_route = static_dir
static_dir = Path(static_dir).resolve() static_dir = Path(os.path.abspath(static_dir))
self.static_dir = static_dir self.static_dir = static_dir
self.static_route = static_route self.static_route = static_route
@@ -81,15 +83,22 @@ class API:
self.debug = debug self.debug = debug
if not allowed_hosts: if not allowed_hosts:
# if not debug:
# raise RuntimeError(
# "You need to specify `allowed_hosts` when debug is set to False"
# )
allowed_hosts = ["*"] allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts self.allowed_hosts = allowed_hosts
if self.static_dir is not None: if self.static_dir is not None:
self.static_dir.mkdir(parents=True, exist_ok=True) os.makedirs(self.static_dir, exist_ok=True)
if self.static_dir is not None:
self.mount(self.static_route, self.static_app) self.mount(self.static_route, self.static_app)
self.formats = get_formats() self.formats = get_formats()
# Cached requests session.
self._session = None self._session = None
self.default_endpoint = None self.default_endpoint = None
@@ -107,19 +116,11 @@ class API:
self.add_middleware(SessionMiddleware, secret_key=self.secret_key) self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
if openapi or docs_route: if openapi or docs_route:
try:
from .ext.openapi import OpenAPISchema
except ImportError as ex:
raise ImportError(
"The dependencies for the OpenAPI extension are not installed. "
"Install them using: pip install responder"
) from ex
self.openapi = OpenAPISchema( self.openapi = OpenAPISchema(
app=self, app=self,
title=title, title=title,
version=version, version=version,
openapi=openapi, openapi=openapi_version,
docs_route=docs_route, docs_route=docs_route,
description=description, description=description,
terms_of_service=terms_of_service, terms_of_service=terms_of_service,
@@ -127,26 +128,13 @@ class API:
license=license, license=license,
openapi_route=openapi_route, openapi_route=openapi_route,
static_route=static_route, static_route=static_route,
openapi_theme=openapi_theme,
) )
# TODO: Update docs for templates
self.templates = Templates(directory=templates_dir) self.templates = Templates(directory=templates_dir)
self.requests = (
if request_id: self.session()
import uuid as _uuid ) #: A Requests session that is connected to the ASGI app.
def _add_request_id(req, resp):
rid = req.headers.get(
"X-Request-ID", str(_uuid.uuid4())
)
resp.headers["X-Request-ID"] = rid
self.router.after_request(_add_request_id)
@property
def requests(self):
"""A test client connected to the ASGI app. Lazily initialized."""
return self.session()
@property @property
def static_app(self): def static_app(self):
@@ -162,79 +150,12 @@ class API:
return decorator return decorator
def after_request(self):
"""Register a function to run after every request.
Usage::
@api.after_request()
def add_request_id(req, resp):
resp.headers["X-Request-ID"] = str(uuid.uuid4())
"""
def decorator(f):
self.router.after_request(f)
return f
return decorator
def add_middleware(self, middleware_cls, **middleware_config): def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config) self.app = middleware_cls(self.app, **middleware_config)
def exception_handler(self, exception_cls):
"""Register a handler for a specific exception type.
Usage::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
"""
def decorator(func):
async def _handler(request, exc):
from starlette.responses import Response as StarletteResp
req = Request(request.scope, request.receive, formats=get_formats())
resp = Response(req=req, formats=get_formats())
if asyncio.iscoroutinefunction(func):
await func(req, resp, exc)
else:
func(req, resp, exc)
if resp.status_code is None:
resp.status_code = 500
body, headers = await resp.body
return StarletteResp(
content=body, status_code=resp.status_code, headers=headers
)
# Register with the ExceptionMiddleware
self.router._exception_handlers = getattr(
self.router, "_exception_handlers", {}
)
self.router._exception_handlers[exception_cls] = _handler
# Also register on the ASGI app chain
from starlette.middleware.exceptions import ExceptionMiddleware as EM
app = self.app
while app is not None:
if isinstance(app, EM):
app.add_exception_handler(exception_cls, _handler)
break
app = getattr(app, "app", None)
return func
return decorator
def schema(self, name, **options): def schema(self, name, **options):
""" """Decorator for creating new routes around function and class definitions.
Decorator for creating new routes around function and class definitions.
Usage:: Usage::
from marshmallow import Schema, fields from marshmallow import Schema, fields
@api.schema("Pet") @api.schema("Pet")
class PetSchema(Schema): class PetSchema(Schema):
@@ -251,12 +172,11 @@ class API:
"""Given a path portion of a URL, tests that it matches against any registered route. """Given a path portion of a URL, tests that it matches against any registered route.
:param path: The path portion of a URL, to test all known routes against. :param path: The path portion of a URL, to test all known routes against.
""" # noqa: E501 (Line too long) """
for route in self.router.routes: for route in self.router.routes:
match, _ = route.matches(path) match, _ = route.matches(path)
if match: if match:
return route return route
return None
def add_route( def add_route(
self, self,
@@ -268,18 +188,16 @@ class API:
check_existing=True, check_existing=True,
websocket=False, websocket=False,
before_request=False, before_request=False,
methods=None,
): ):
"""Adds a route to the API. """Adds a route to the API.
:param route: A string representation of the route. :param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, or a class. :param endpoint: The endpoint for the route -- can be a callable, or a class.
:param default: If ``True``, all unknown requests will route to this view. :param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html". :param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
Also, it will become a default route. """
:param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).
""" # noqa: E501
# Path
if static: if static:
assert self.static_dir is not None assert self.static_dir is not None
if not endpoint: if not endpoint:
@@ -293,35 +211,27 @@ class API:
websocket=websocket, websocket=websocket,
before_request=before_request, before_request=before_request,
check_existing=check_existing, check_existing=check_existing,
methods=methods,
) )
async def _static_response(self, req, resp): async def _static_response(self, req, resp):
assert self.static_dir is not None assert self.static_dir is not None
index = (self.static_dir / "index.html").resolve() index = (self.static_dir / "index.html").resolve()
if index.exists(): if os.path.exists(index):
resp.html = index.read_text() with open(index, "r") as f:
resp.html = f.read()
else: else:
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined] resp.status_code = status_codes.HTTP_404
resp.text = "Not found." resp.text = "Not found."
def redirect( def redirect(
self, self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
resp,
location,
*,
set_text=True,
status_code=status_codes.HTTP_301, # type: ignore[attr-defined]
): ):
""" """Redirects a given response to a given location.
Redirects a given response to a given location.
:param resp: The Response to mutate. :param resp: The Response to mutate.
:param location: The location of the redirect. :param location: The location of the redirect.
:param set_text: If ``True``, sets the Redirect body content automatically. :param set_text: If ``True``, sets the Redirect body content automatically.
:param status_code: an `API.status_codes` attribute, or an integer, :param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
representing the HTTP status code of the redirect.
""" """
resp.redirect(location, set_text=set_text, status_code=status_code) resp.redirect(location, set_text=set_text, status_code=status_code)
@@ -354,9 +264,9 @@ class API:
:param handler: The function to run. Can be either a function or a coroutine. :param handler: The function to run. Can be either a function or a coroutine.
""" """
self.router.add_event_handler(event_type, handler) self.router.lifespan_handler.add_event_handler(event_type, handler)
def route(self, route=None, *, request_model=None, response_model=None, **options): def route(self, route=None, **options):
"""Decorator for creating new routes around function and class definitions. """Decorator for creating new routes around function and class definitions.
Usage:: Usage::
@@ -365,89 +275,34 @@ class API:
def hello(req, resp): def hello(req, resp):
resp.text = "hello, world!" resp.text = "hello, world!"
With Pydantic models for OpenAPI documentation::
from pydantic import BaseModel
class ItemIn(BaseModel):
name: str
price: float
class ItemOut(BaseModel):
id: int
name: str
price: float
@api.route("/items", methods=["POST"],
request_model=ItemIn, response_model=ItemOut)
async def create_item(req, resp):
data = await req.media()
resp.media = {"id": 1, **data}
""" """
def decorator(f): def decorator(f):
if request_model is not None:
f._request_model = request_model
if hasattr(self, "openapi"):
self.openapi.add_schema(
request_model.__name__, request_model, check_existing=False
)
if response_model is not None:
f._response_model = response_model
if hasattr(self, "openapi"):
self.openapi.add_schema(
response_model.__name__, response_model, check_existing=False
)
self.add_route(route, f, **options) self.add_route(route, f, **options)
return f return f
return decorator return decorator
def graphql(self, route="/graphql", *, schema):
"""Mount a GraphQL API at the given route.
Usage::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
:param route: The URL path for the GraphQL endpoint.
:param schema: A Graphene schema instance.
"""
from .ext.graphql import GraphQLView
self.add_route(route, GraphQLView(api=self, schema=schema))
def mount(self, route, app): def mount(self, route, app):
"""Mounts an WSGI / ASGI application at a given route. """Mounts an WSGI / ASGI application at a given route.
:param route: String representation of the route to be used :param route: String representation of the route to be used (shouldn't be parameterized).
(shouldn't be parameterized).
:param app: The other WSGI / ASGI app. :param app: The other WSGI / ASGI app.
""" """
self.router.apps.update({route: app}) self.router.apps.update({route: app})
def session(self, base_url="http://;"): def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a Starlette TestClient instance, """Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the Responder application.
able to send HTTP requests to the Responder application.
:param base_url: The base URL for the test client. :param base_url: The URL to mount the connection adaptor to.
""" """
if self._session is None: if self._session is None:
from starlette.testclient import TestClient
self._session = TestClient(self, base_url=base_url) self._session = TestClient(self, base_url=base_url)
return self._session return self._session
def url_for(self, endpoint, **params): def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route. """Given an endpoint, returns a rendered URL for its route.
:param endpoint: The route endpoint you're searching for. :param endpoint: The route endpoint you're searching for.
@@ -456,87 +311,53 @@ class API:
return self.router.url_for(endpoint, **params) return self.router.url_for(endpoint, **params)
def template(self, filename, *args, **kwargs): def template(self, filename, *args, **kwargs):
r"""Render a Jinja2 template file with the provided values. """Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param filename: The filename of the jinja2 template, in ``templates_dir``. :param filename: The filename of the jinja2 template, in ``templates_dir``.
:param \*args: Data to pass into the template. :param *args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template. :param *kwargs: Date to pass into the template.
""" """
return self.templates.render(filename, *args, **kwargs) return self.templates.render(filename, *args, **kwargs)
def template_string(self, source, *args, **kwargs): def template_string(self, source, *args, **kwargs):
r"""Render a Jinja2 template string with the provided values. """Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param source: The template to use, a Jinja2 template string. :param source: The template to use.
:param \*args: Data to pass into the template. :param *args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template. :param **kwargs: Data to pass into the template.
""" """
return self.templates.render_string(source, *args, **kwargs) return self.templates.render_string(source, *args, **kwargs)
def serve(self, *, address=None, port=None, debug=False, **options): def serve(self, *, address=None, port=None, debug=False, **options):
""" """Runs the application with uvicorn. If the ``PORT`` environment
Run the application with uvicorn. variable is set, requests will be served on that port automatically to all
known hosts.
If the ``PORT`` environment variable is set, requests will be served on that port
automatically to all known hosts.
:param address: The address to bind to. :param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random. :param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: Whether to run application in debug mode. :param debug: Run uvicorn server in debug mode.
:param options: Additional keyword arguments to send to ``uvicorn.run()``. :param options: Additional keyword arguments to send to ``uvicorn.run()``.
""" # noqa: E501 """
if "PORT" in os.environ: if "PORT" in os.environ:
if address is None: if address is None:
address = "0.0.0.0" # noqa: S104 address = "0.0.0.0"
port = int(os.environ["PORT"]) port = int(os.environ["PORT"])
if address is None: if address is None:
address = "127.0.0.1" address = "127.0.0.1"
if port is None: if port is None:
port = 5042 port = 5042
if debug:
options["log_level"] = "debug"
uvicorn.run(self, host=address, port=port, **options) def spawn():
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
def run(self, **kwargs): def run(self, **kwargs):
if "debug" not in kwargs: if "debug" not in kwargs:
kwargs.update({"debug": self.debug}) kwargs.update({"debug": self.debug})
self.serve(**kwargs) self.serve(**kwargs)
def group(self, prefix):
"""Create a route group with a shared URL prefix.
Usage::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{id:int}")
def get_user(req, resp, *, id):
resp.media = {"id": id}
"""
return RouteGroup(api=self, prefix=prefix)
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
await self.app(scope, receive, send) await self.app(scope, receive, send)
class RouteGroup:
"""A group of routes with a shared URL prefix."""
def __init__(self, api, prefix):
self.api = api
self.prefix = prefix.rstrip("/")
def route(self, route=None, **options):
full_route = f"{self.prefix}{route}"
return self.api.route(full_route, **options)
def before_request(self, **kwargs):
return self.api.before_request(**kwargs)

Some files were not shown because too many files have changed in this diff Show More