mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a46a87b3e |
@@ -1,3 +0,0 @@
|
||||
github: kennethreitz
|
||||
thanks_dev: kennethreitz
|
||||
custom: https://cash.app/$KennethReitz
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +1,3 @@
|
||||
.venv*
|
||||
.vscode/
|
||||
.cache
|
||||
.idea
|
||||
@@ -7,7 +6,6 @@
|
||||
.pytest_cache
|
||||
.DS_Store
|
||||
coverage.xml
|
||||
.coverage*
|
||||
|
||||
__pycache__
|
||||
tests/__pycache__
|
||||
|
||||
@@ -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
@@ -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
@@ -1,422 +1,274 @@
|
||||
# Changelog
|
||||
|
||||
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
|
||||
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix static app resolving
|
||||
|
||||
## [v2.0.3] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.2] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template conflicts
|
||||
|
||||
## [v2.0.1] - 2019-09-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix template import
|
||||
|
||||
## [v2.0.0] - 2019-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor Router and Schema
|
||||
|
||||
## [v1.3.2] - 2019-08-15
|
||||
|
||||
### Added
|
||||
|
||||
- ASGI 3 support
|
||||
- CI tests for python 3.8-dev
|
||||
- Now requests have `state` a mapping object
|
||||
|
||||
### Deprecated
|
||||
|
||||
- ASGI 2
|
||||
|
||||
## [v1.3.1] - 2019-04-28
|
||||
|
||||
### Added
|
||||
|
||||
- Route params Converters
|
||||
- Add search for documentation pages
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump dependencies
|
||||
|
||||
## [v1.3.0] - 2019-02-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Versioning issue
|
||||
- Multiple cookies.
|
||||
- Whitenoise returns not found.
|
||||
- Other bugfixes.
|
||||
|
||||
### Added
|
||||
|
||||
- Stream support via `resp.stream`.
|
||||
- Cookie directives via `resp.set_cookie`.
|
||||
- Add `resp.html` to send HTML.
|
||||
- Other improvements.
|
||||
|
||||
## [v1.1.3] - 2019-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor `_route_for`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolve startup/shutdwown events
|
||||
|
||||
## [v1.2.0] - 2018-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- Documentations
|
||||
|
||||
### Changed
|
||||
|
||||
- Use Starlette's LifeSpan middleware
|
||||
- Update denpendencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix route.is_class_based
|
||||
- Fix test_500
|
||||
- Typos
|
||||
|
||||
## [v1.1.2] - 2018-11-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Minor fixes for Open API
|
||||
- Typos
|
||||
|
||||
## [v1.1.1] - 2018-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Run sync views in a threadpoolexecutor.
|
||||
|
||||
## [v1.1.0] - 2018-10-27
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `before_request`.
|
||||
|
||||
## [v1.0.5]- 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sessions.
|
||||
|
||||
## [v1.0.4] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Potential bufix for cookies.
|
||||
|
||||
## [v1.0.3] - 2018-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for redirects.
|
||||
|
||||
## [v1.0.2] - 2018-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement for static file hosting.
|
||||
|
||||
## [v1.0.1] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cors configuration settings.
|
||||
|
||||
## [v1.0.0] - 2018-10-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Move GraphQL support into a built-in plugin.
|
||||
|
||||
## [v0.3.3] - 2018-10-25
|
||||
|
||||
### Added
|
||||
|
||||
- CORS support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved exceptions.
|
||||
|
||||
## [v0.3.2] - 2018-10-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Subtle improvements.
|
||||
|
||||
## [v0.3.1] - 2018-10-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Packaging fix.
|
||||
|
||||
## [v0.3.0] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Interactive Documentation endpoint.
|
||||
- Minor improvements.
|
||||
|
||||
## [v0.2.3] - 2018-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Overall improvements.
|
||||
|
||||
## [v0.2.2] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- Show traceback info when background tasks raise exceptions.
|
||||
|
||||
## [v0.2.1] - 2018-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- api.requests.
|
||||
|
||||
## [v0.2.0] - 2018-10-22
|
||||
|
||||
### Added
|
||||
|
||||
- WebSocket support.
|
||||
|
||||
## [v0.1.6] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- 500 support.
|
||||
|
||||
## [v0.1.5] - 2018-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- File upload support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvements to sequential media reading.
|
||||
|
||||
## [v0.1.4] - 2018-10-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stability.
|
||||
|
||||
## [v0.1.3] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Sessions support.
|
||||
|
||||
## [v0.1.2] - 2018-10-18
|
||||
|
||||
### Added
|
||||
|
||||
- Cookies support.
|
||||
|
||||
## [v0.1.1] - 2018-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Default routes.
|
||||
|
||||
## [v0.1.0] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- Prototype of static application support.
|
||||
|
||||
## [v0.0.10] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.9] - 2018-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for async class-based views.
|
||||
|
||||
## [v0.0.8] - 2018-10-17
|
||||
|
||||
### Added
|
||||
|
||||
- GraphiQL Support.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvement to route selection.
|
||||
|
||||
## [v0.0.7] - 2018-10-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Immutable Request object.
|
||||
|
||||
## [v0.0.6] - 2018-10-16
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to mount WSGI apps.
|
||||
- Supply content-type when serving up the schema.
|
||||
|
||||
## [v0.0.5] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- OpenAPI Schema support.
|
||||
- Safe load/dump yaml.
|
||||
|
||||
## [v0.0.4] - 2018-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- Asynchronous support for data uploads.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.3] - 2018-10-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## [v0.0.2] - 2018-10-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to ASGI/Starlette.
|
||||
|
||||
## [v0.0.1] - 2018-10-12
|
||||
|
||||
### Added
|
||||
|
||||
- Conception!
|
||||
|
||||
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.0.0..HEAD
|
||||
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
|
||||
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
|
||||
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
|
||||
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
|
||||
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
|
||||
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
|
||||
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
|
||||
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
|
||||
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
|
||||
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
|
||||
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
|
||||
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
|
||||
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
|
||||
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
|
||||
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
|
||||
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
|
||||
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
|
||||
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
|
||||
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
|
||||
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
|
||||
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
|
||||
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
|
||||
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
|
||||
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
|
||||
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
|
||||
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
|
||||
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
|
||||
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
|
||||
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
|
||||
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
|
||||
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
|
||||
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
|
||||
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
|
||||
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
|
||||
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
|
||||
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
|
||||
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
|
||||
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
|
||||
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
|
||||
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
|
||||
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
|
||||
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
|
||||
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
|
||||
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
|
||||
[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
|
||||
[Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.4..HEAD
|
||||
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
|
||||
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
|
||||
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
|
||||
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
|
||||
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
|
||||
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
||||
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
||||
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
||||
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
|
||||
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
|
||||
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
|
||||
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
|
||||
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
|
||||
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
|
||||
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
|
||||
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
|
||||
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
|
||||
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
|
||||
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
|
||||
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
|
||||
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
|
||||
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
|
||||
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
|
||||
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
|
||||
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
|
||||
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
|
||||
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
|
||||
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
|
||||
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
|
||||
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
|
||||
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
|
||||
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
|
||||
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
|
||||
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
|
||||
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
|
||||
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
|
||||
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
|
||||
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
|
||||
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
|
||||
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
|
||||
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
|
||||
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
|
||||
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
|
||||
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
|
||||
|
||||
@@ -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
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
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
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include LICENSE
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/).
|
||||
[](https://travis-ci.org/taoufik07/responder)
|
||||
[](https://responder.readthedocs.io/en/latest/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://pypi.org/project/responder/)
|
||||
[](https://github.com/taoufik07/responder/graphs/contributors)
|
||||
|
||||
```python
|
||||
import responder
|
||||
[](https://responder.readthedocs.io)
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/{greeting}")
|
||||
async def greet_world(req, resp, *, greeting):
|
||||
resp.text = f"{greeting}, world!"
|
||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
```
|
||||
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.
|
||||
|
||||
$ 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.
|
||||
- `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.
|
||||
> "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/)
|
||||
|
||||
## 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
|
||||
@api.route("/items", methods=["POST"])
|
||||
async def create_item(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"created": data}
|
||||
## More Examples
|
||||
|
||||
# Class-based views
|
||||
@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"
|
||||
See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
|
||||
|
||||
# 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
|
||||
@api.exception_handler(ValueError)
|
||||
async def handle_error(req, resp, exc):
|
||||
resp.status_code = 400
|
||||
resp.media = {"error": str(exc)}
|
||||
# Installing Responder
|
||||
|
||||
# Lifespan events
|
||||
from contextlib import asynccontextmanager
|
||||
Install the stable release:
|
||||
|
||||
@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
|
||||
@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}!")
|
||||
Or, install from the development branch:
|
||||
|
||||
# Mount WSGI/ASGI apps
|
||||
from flask import Flask
|
||||
flask_app = Flask(__name__)
|
||||
api.mount("/flask", flask_app)
|
||||
$ pipenv install -e git+https://github.com/taoufik07/responder.git#egg=responder
|
||||
|
||||
# Background tasks
|
||||
@api.route("/work")
|
||||
def do_work(req, resp):
|
||||
@api.background.task
|
||||
def process():
|
||||
import time; time.sleep(10)
|
||||
process()
|
||||
resp.media = {"status": "processing"}
|
||||
```
|
||||
Only **Python 3.6+** is supported.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
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. -->
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
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
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
Binary file not shown.
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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
/* 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 {
|
||||
width: 1008px;
|
||||
}
|
||||
|
||||
/* Better spacing around code blocks. */
|
||||
/* Much-improved spacing around code blocks. */
|
||||
div.highlight pre {
|
||||
padding: 11px 14px;
|
||||
}
|
||||
|
||||
/* Responsive layout. */
|
||||
/* Remain Responsive! */
|
||||
@media screen and (max-width: 1008px) {
|
||||
div.sphinxsidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Have code blocks escape the document right-margin. */
|
||||
div.highlight pre {
|
||||
margin-right: -30px;
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -1,14 +1,55 @@
|
||||
<p class="logo">
|
||||
<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>
|
||||
</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>
|
||||
<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="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 +0,0 @@
|
||||
../../CHANGELOG.md
|
||||
@@ -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
@@ -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"
|
||||
copyright = "2018-2026, Kenneth Reitz"
|
||||
copyright = "2018, A Kenneth Reitz project"
|
||||
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__))
|
||||
|
||||
about = {}
|
||||
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
version = about["__version__"]
|
||||
# The full version, including alpha/beta/rc tags
|
||||
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 = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.mathjax",
|
||||
"sphinx.ext.ifconfig",
|
||||
"sphinx.ext.viewcode",
|
||||
"myst_parser",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_design_elements",
|
||||
"sphinx.ext.githubpages",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
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"
|
||||
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 = []
|
||||
|
||||
# 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"
|
||||
|
||||
# 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 = {
|
||||
"show_powered_by": False,
|
||||
"github_user": "kennethreitz",
|
||||
@@ -37,16 +105,118 @@ html_theme_options = {
|
||||
"github_banner": False,
|
||||
"show_related": False,
|
||||
}
|
||||
html_static_path = ["_static"]
|
||||
|
||||
|
||||
html_sidebars = {
|
||||
"index": ["sidebarintro.html", "searchbox.html"],
|
||||
"**": ["sidebarintro.html", "localtoc.html", "searchbox.html"],
|
||||
"index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"],
|
||||
"**": [
|
||||
"sidebarlogo.html",
|
||||
"localtoc.html",
|
||||
"relations.html",
|
||||
"sourcelink.html",
|
||||
"searchbox.html",
|
||||
"hacks.html",
|
||||
],
|
||||
}
|
||||
|
||||
# MyST
|
||||
myst_heading_anchors = 3
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# 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
|
||||
copybutton_remove_prompts = True
|
||||
copybutton_prompt_text = r">>> |\.\.\. |\$ "
|
||||
copybutton_prompt_is_regexp = True
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# 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
@@ -1,86 +1,58 @@
|
||||
Deployment
|
||||
==========
|
||||
Deploying Responder
|
||||
===================
|
||||
|
||||
Responder applications are standard ASGI apps. You can deploy them anywhere
|
||||
you'd deploy a Python web service.
|
||||
You can deploy Responder anywhere you can deploy a basic Python application.
|
||||
|
||||
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
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
async def hello(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
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::
|
||||
|
||||
FROM python:3.13-slim
|
||||
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.
|
||||
$ git add -A
|
||||
$ git commit -m 'initial commit'
|
||||
$ git push heroku master
|
||||
|
||||
+102
-71
@@ -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
|
||||
|
||||
@@ -16,96 +34,109 @@ A familiar HTTP Service Framework for Python.
|
||||
if __name__ == '__main__':
|
||||
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
|
||||
together into one clean framework.
|
||||
- A pleasant API, with a single import statement.
|
||||
- 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
|
||||
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
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
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.
|
||||
- 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.
|
||||
—Rudraksh M.K.
|
||||
|
||||
|
||||
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::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
|
||||
quickstart
|
||||
tour
|
||||
deployment
|
||||
testing
|
||||
api
|
||||
cli
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Project
|
||||
|
||||
changes
|
||||
Sandbox <sandbox>
|
||||
backlog
|
||||
|
||||
|
||||
.. _Starlette: https://www.starlette.io/
|
||||
.. _uvicorn: https://www.uvicorn.org/
|
||||
.. _Flask: https://flask.palletsprojects.com/
|
||||
.. _Falcon: https://falconframework.org/
|
||||
.. _FastAPI: https://fastapi.tiangolo.com/
|
||||
.. _GraphQL: https://graphql.org/
|
||||
Installing Responder
|
||||
--------------------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ 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
@@ -1,234 +1,177 @@
|
||||
Quick Start
|
||||
===========
|
||||
Quick Start!
|
||||
============
|
||||
|
||||
This guide will walk you through the basics of building a web service with
|
||||
Responder. By the end, you'll know how to declare routes, handle requests,
|
||||
send responses, render templates, and process background tasks.
|
||||
This section of the documentation exists to provide an introduction to the Responder interface,
|
||||
as well as educate the user on basic functionality.
|
||||
|
||||
|
||||
Create a Web Service
|
||||
--------------------
|
||||
Declare a Web Service
|
||||
---------------------
|
||||
|
||||
The first thing you need to do is declare a web service. This is the central
|
||||
object that holds all your routes, middleware, and configuration::
|
||||
The first thing you need to do is declare a web service::
|
||||
|
||||
import responder
|
||||
|
||||
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("/")
|
||||
def hello_world(req, resp):
|
||||
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
|
||||
--------------
|
||||
|
||||
Start your web service with ``api.run()``::
|
||||
Next, we can run our web service easily, with ``api.run()``::
|
||||
|
||||
api.run()
|
||||
|
||||
This spins up a production-grade uvicorn server on port ``5042``, ready for
|
||||
incoming HTTP requests.
|
||||
This will spin up a production web server on port ``5042``, ready for incoming HTTP requests.
|
||||
|
||||
You can customize the port with ``api.run(port=8000)``. The ``PORT``
|
||||
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.
|
||||
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).
|
||||
|
||||
|
||||
Route Parameters
|
||||
----------------
|
||||
Accept Route Arguments
|
||||
----------------------
|
||||
|
||||
If you want dynamic URLs, use Python's familiar f-string syntax to declare
|
||||
variables in your routes::
|
||||
If you want dynamic URLs, you can use Python's familiar *f-string syntax* to declare variables in your routes::
|
||||
|
||||
@api.route("/hello/{who}")
|
||||
def hello_to(req, resp, *, 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
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
You can constrain route parameters to specific types. The parameter will be
|
||||
automatically converted before it reaches your view::
|
||||
Type convertors are also available::
|
||||
|
||||
@api.route("/add/{a:int}/{b:int}")
|
||||
async def add(req, resp, *, a, b):
|
||||
resp.text = f"{a} + {b} = {a + b}"
|
||||
|
||||
Supported types:
|
||||
Supported types: ``str``, ``int`` and ``float``.
|
||||
|
||||
- ``str`` — matches any string without slashes (default)
|
||||
- ``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
|
||||
Returning JSON / YAML
|
||||
---------------------
|
||||
|
||||
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")
|
||||
def hello_json(req, resp, *, who):
|
||||
def hello_to(req, resp, *, who):
|
||||
resp.media = {"hello": who}
|
||||
|
||||
If the client sends an ``Accept: application/x-yaml`` header, the same data
|
||||
will be returned as YAML instead. Content negotiation is automatic.
|
||||
A ``GET`` request to ``/hello/guido/json`` will result in a response of ``{'hello': 'guido'}``.
|
||||
|
||||
**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
|
||||
resp.headers["X-Custom"] = "value"
|
||||
templates = Templates()
|
||||
|
||||
**Redirects**::
|
||||
|
||||
api.redirect(resp, location="/new-url")
|
||||
@api.route("/hello/{name}/html")
|
||||
def hello(req, resp, name):
|
||||
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)
|
||||
req.full_url # "http://example.com/path?q=1"
|
||||
req.url # parsed URL object
|
||||
|
||||
**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
|
||||
@api.route("/hello/{who}/html")
|
||||
def hello_html(req, resp, *, who):
|
||||
resp.html = api.template('hello.html', who=who)
|
||||
|
||||
|
||||
Rendering Templates
|
||||
-------------------
|
||||
Setting Response Status Code
|
||||
----------------------------
|
||||
|
||||
Responder includes built-in `Jinja2 <https://jinja.palletsprojects.com/>`_
|
||||
support. Templates are loaded from the ``templates/`` directory by default.
|
||||
If you want to set the response status code, simply set ``resp.status_code``::
|
||||
|
||||
The simplest way is to use ``api.template()``::
|
||||
|
||||
@api.route("/hello/{name}/html")
|
||||
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")
|
||||
@api.route("/416")
|
||||
def teapot(req, resp):
|
||||
resp.status_code = api.status_codes.HTTP_416 # ...or 416
|
||||
|
||||
|
||||
Background Tasks
|
||||
----------------
|
||||
Setting Response Headers
|
||||
------------------------
|
||||
|
||||
Sometimes you want to accept a request, respond immediately, and do the
|
||||
actual processing later. Responder makes this easy with background tasks::
|
||||
If you want to set a response header, like ``X-Pizza: 42``, simply modify the ``resp.headers`` dictionary::
|
||||
|
||||
@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")
|
||||
async def receive_incoming(req, resp):
|
||||
data = await req.media()
|
||||
|
||||
@api.background.task
|
||||
def process_data(data):
|
||||
"""This runs in a background thread."""
|
||||
import time
|
||||
time.sleep(10) # simulate heavy work
|
||||
"""Just sleeps for three seconds, as a demo."""
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# Respond immediately — processing continues in the background
|
||||
resp.media = {"status": "accepted"}
|
||||
# Immediately respond that upload was successful.
|
||||
resp.media = {'success': True}
|
||||
|
||||
The ``@api.background.task`` decorator wraps any function to run in a thread
|
||||
pool. The client gets an immediate response while the work continues.
|
||||
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
@@ -1,46 +1,54 @@
|
||||
Testing
|
||||
=======
|
||||
Building and Testing with Responder
|
||||
===================================
|
||||
|
||||
Responder includes a built-in test client powered by Starlette's
|
||||
``TestClient``. You don't need to start a server — tests run in-process,
|
||||
making them fast and reliable.
|
||||
Responder comes with a first-class, well supported test client for your ASGI web services: **Requests**.
|
||||
|
||||
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
|
||||
|
||||
api = responder.API()
|
||||
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
def hello_world(req, resp):
|
||||
resp.text = "hello, world!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.run()
|
||||
|
||||
You can test it with pytest::
|
||||
|
||||
# test_api.py
|
||||
import api as service
|
||||
``$ cat Pipfile``::
|
||||
|
||||
def test_hello():
|
||||
r = service.api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
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
|
||||
across tests::
|
||||
Writing Tests
|
||||
-------------
|
||||
|
||||
``$ cat test_api.py``::
|
||||
|
||||
import pytest
|
||||
import api as service
|
||||
@@ -49,109 +57,25 @@ across tests::
|
||||
def api():
|
||||
return service.api
|
||||
|
||||
def test_hello(api):
|
||||
|
||||
def test_hello_world(api):
|
||||
r = api.requests.get("/")
|
||||
assert r.text == "hello, world!"
|
||||
|
||||
def test_json(api):
|
||||
@api.route("/data")
|
||||
def data(req, resp):
|
||||
resp.media = {"key": "value"}
|
||||
``$ pytest``::
|
||||
|
||||
r = api.requests.get(api.url_for(data))
|
||||
assert r.json() == {"key": "value"}
|
||||
|
||||
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.
|
||||
...
|
||||
========================== 1 passed in 0.10 seconds ==========================
|
||||
|
||||
|
||||
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):
|
||||
@api.route("/items")
|
||||
async def create(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"created": data}
|
||||
resp.status_code = 201
|
||||
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
|
||||
2. ``$ pipenv install -e . --dev``
|
||||
|
||||
r = api.requests.post(api.url_for(create), json={"name": "widget"})
|
||||
assert r.status_code == 201
|
||||
assert r.json() == {"created": {"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``.
|
||||
|
||||
|
||||
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}
|
||||
This will ensure that your application gets installed in every developer's environment, using Pipenv.
|
||||
|
||||
+326
-407
@@ -1,177 +1,41 @@
|
||||
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
|
||||
-----------------
|
||||
|
||||
For more complex resources, you can use class-based views. Responder will
|
||||
dispatch to the appropriate method handler based on the HTTP method::
|
||||
Class-based views (and setting some headers and stuff)::
|
||||
|
||||
@api.route("/{greeting}")
|
||||
class GreetingResource:
|
||||
def on_get(self, req, resp, *, greeting):
|
||||
def on_request(self, req, resp, *, greeting): # or on_get...
|
||||
resp.text = f"{greeting}, world!"
|
||||
|
||||
def on_post(self, req, resp, *, greeting):
|
||||
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.
|
||||
resp.headers.update({'X-Life': '42'})
|
||||
resp.status_code = api.status_codes.HTTP_416
|
||||
|
||||
|
||||
Lifespan Events
|
||||
---------------
|
||||
Background Tasks
|
||||
----------------
|
||||
|
||||
Modern applications often need to set up resources on startup (database
|
||||
connections, caches, ML models) and tear them down on shutdown. Responder
|
||||
supports the lifespan context manager pattern::
|
||||
Here, you can spawn off a background thread to run any function, out-of-request::
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
@api.route("/")
|
||||
def hello(req, resp):
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup — runs before the first request
|
||||
print("connecting to database...")
|
||||
yield
|
||||
# Shutdown — runs after the server stops
|
||||
print("closing connections...")
|
||||
@api.background.task
|
||||
def sleep(s=10):
|
||||
time.sleep(s)
|
||||
print("slept!")
|
||||
|
||||
api = responder.API(lifespan=lifespan)
|
||||
|
||||
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
|
||||
sleep()
|
||||
resp.content = "processing"
|
||||
|
||||
|
||||
GraphQL
|
||||
-------
|
||||
|
||||
Responder includes built-in GraphQL support via
|
||||
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
|
||||
with a single method call::
|
||||
Serve a GraphQL API::
|
||||
|
||||
import graphene
|
||||
|
||||
@@ -181,328 +45,383 @@ with a single method call::
|
||||
def resolve_hello(self, info, 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,
|
||||
where you can explore your schema and test queries. Programmatic clients
|
||||
can POST JSON queries to the same endpoint.
|
||||
api.add_route("/graph", view)
|
||||
|
||||
You can access the Responder request and response objects in your resolvers
|
||||
through ``info.context["request"]`` and ``info.context["response"]``.
|
||||
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
|
||||
|
||||
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
|
||||
documentation automatically::
|
||||
Responder comes with built-in support for OpenAPI / marshmallow
|
||||
|
||||
api = responder.API(
|
||||
title="Pet Store",
|
||||
New in Responder `1.4.0`::
|
||||
|
||||
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",
|
||||
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.
|
||||
|
||||
**Pydantic models** — the recommended approach for new APIs. Use
|
||||
``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.
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get all pets
|
||||
description: Get a random pet
|
||||
responses:
|
||||
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
|
||||
|
||||
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")
|
||||
class PetSchema(Schema):
|
||||
name = fields.Str()
|
||||
|
||||
All three approaches can be mixed in the same API. Pydantic models,
|
||||
marshmallow schemas, and YAML docstrings all contribute to the same
|
||||
generated OpenAPI specification.
|
||||
@api.route("/")
|
||||
def route(req, resp):
|
||||
"""A cute furry animal endpoint.
|
||||
---
|
||||
get:
|
||||
description: Get a random pet
|
||||
responses:
|
||||
200:
|
||||
description: A pet to be returned
|
||||
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
|
||||
you can gradually migrate from Flask, or run multiple frameworks side by side::
|
||||
Responder can automatically supply API Documentation for you. Using the example above
|
||||
|
||||
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("/")
|
||||
def hello():
|
||||
return "Hello from Flask!"
|
||||
schema = Schema(
|
||||
app=api,
|
||||
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
|
||||
through Responder. Both WSGI and ASGI apps are supported — Responder
|
||||
wraps WSGI apps automatically.
|
||||
|
||||
|
||||
Cookies
|
||||
-------
|
||||
|
||||
Reading and writing cookies is straightforward::
|
||||
|
||||
# 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="/",
|
||||
api = responder.API(
|
||||
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,
|
||||
)
|
||||
|
||||
Supported directives: ``key``, ``value``, ``expires``, ``max_age``,
|
||||
``domain``, ``path``, ``secure``, ``httponly``.
|
||||
This will make ``/docs`` render interactive documentation for your API.
|
||||
|
||||
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
|
||||
read from and write to the ``session`` dictionary::
|
||||
import responder
|
||||
from flask import Flask
|
||||
|
||||
@api.route("/login")
|
||||
def login(req, resp):
|
||||
resp.session["username"] = "alice"
|
||||
api = responder.API()
|
||||
flask = Flask(__name__)
|
||||
|
||||
@api.route("/profile")
|
||||
def profile(req, resp):
|
||||
resp.media = {"user": req.session.get("username")}
|
||||
@flask.route('/')
|
||||
def hello():
|
||||
return 'hello'
|
||||
|
||||
The session data is stored in a cookie called ``Responder-Session``. It's
|
||||
signed for tamper protection, so you can trust that the data originated
|
||||
from your server.
|
||||
api.mount('/flask', flask)
|
||||
|
||||
.. 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")
|
||||
|
||||
|
||||
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::
|
||||
If you have a single-page webapp, you can tell Responder to serve up your ``static/index.html`` at a route, like so::
|
||||
|
||||
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
|
||||
----
|
||||
|
||||
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
|
||||
origins, methods, and headers your frontend needs.
|
||||
api = responder.API(cors=True)
|
||||
|
||||
|
||||
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::
|
||||
|
||||
api = responder.API(enable_hsts=True)
|
||||
|
||||
This adds the ``Strict-Transport-Security`` header and redirects HTTP
|
||||
requests to HTTPS.
|
||||
In order to set custom parameters, you need to set the ``cors_params`` argument of ``api``, a dictionary containing the following entries:
|
||||
|
||||
* ``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
|
||||
-------------
|
||||
|
||||
Protect against HTTP Host header attacks by restricting which hostnames
|
||||
your application will respond to::
|
||||
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.
|
||||
|
||||
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
|
||||
great for live feeds, progress updates, and AI streaming responses::
|
||||
|
||||
@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.
|
||||
* By default, all hostnames are allowed.
|
||||
* Wildcard domains such as ``*.example.com`` are supported.
|
||||
* To allow any hostname use ``allowed_hosts=["*"]``.
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
;addopts= -rsxX -s -v --strict
|
||||
filterwarnings =
|
||||
error::UserWarning
|
||||
@@ -0,0 +1,5 @@
|
||||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
+1
-17
@@ -1,18 +1,2 @@
|
||||
"""
|
||||
Responder - a familiar HTTP Service Framework.
|
||||
|
||||
This module exports the core functionality of the Responder framework,
|
||||
including the API, Request, and Response classes.
|
||||
"""
|
||||
|
||||
from .core import *
|
||||
from . import ext
|
||||
from .__version__ import __version__
|
||||
from .core import API, Request, Response
|
||||
|
||||
__all__ = [
|
||||
"API",
|
||||
"Request",
|
||||
"Response",
|
||||
"__version__",
|
||||
"ext",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "2.0.4"
|
||||
|
||||
+75
-254
@@ -1,37 +1,41 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["API"]
|
||||
|
||||
import jinja2
|
||||
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.exceptions import ExceptionMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
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 .formats import get_formats
|
||||
from .models import Request, Response
|
||||
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 .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
|
||||
from .templates import Templates
|
||||
|
||||
|
||||
class API:
|
||||
"""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 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 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
|
||||
: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 auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
|
||||
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
|
||||
"""
|
||||
|
||||
status_codes = status_codes
|
||||
|
||||
@@ -40,12 +44,13 @@ class API:
|
||||
*,
|
||||
debug=False,
|
||||
title=None,
|
||||
version=None,
|
||||
version="1.0",
|
||||
description=None,
|
||||
terms_of_service=None,
|
||||
contact=None,
|
||||
license=None, # noqa: A002
|
||||
license=None,
|
||||
openapi=None,
|
||||
openapi_version="3.0.2",
|
||||
openapi_route="/schema.yml",
|
||||
static_dir="static",
|
||||
static_route="/static",
|
||||
@@ -57,20 +62,17 @@ class API:
|
||||
cors=False,
|
||||
cors_params=DEFAULT_CORS_PARAMS,
|
||||
allowed_hosts=None,
|
||||
openapi_theme=DEFAULT_OPENAPI_THEME,
|
||||
lifespan=None,
|
||||
request_id=False,
|
||||
):
|
||||
self.background = BackgroundQueue()
|
||||
|
||||
self.secret_key = secret_key
|
||||
|
||||
self.router = Router(lifespan=lifespan)
|
||||
self.router = Router()
|
||||
|
||||
if static_dir is not None:
|
||||
if static_route is None:
|
||||
static_route = ""
|
||||
static_dir = Path(static_dir).resolve()
|
||||
static_route = static_dir
|
||||
static_dir = Path(os.path.abspath(static_dir))
|
||||
|
||||
self.static_dir = static_dir
|
||||
self.static_route = static_route
|
||||
@@ -81,15 +83,22 @@ class API:
|
||||
self.debug = debug
|
||||
|
||||
if not allowed_hosts:
|
||||
# if not debug:
|
||||
# raise RuntimeError(
|
||||
# "You need to specify `allowed_hosts` when debug is set to False"
|
||||
# )
|
||||
allowed_hosts = ["*"]
|
||||
self.allowed_hosts = allowed_hosts
|
||||
|
||||
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.formats = get_formats()
|
||||
|
||||
# Cached requests session.
|
||||
self._session = None
|
||||
|
||||
self.default_endpoint = None
|
||||
@@ -107,19 +116,11 @@ class API:
|
||||
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
|
||||
|
||||
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(
|
||||
app=self,
|
||||
title=title,
|
||||
version=version,
|
||||
openapi=openapi,
|
||||
openapi=openapi_version,
|
||||
docs_route=docs_route,
|
||||
description=description,
|
||||
terms_of_service=terms_of_service,
|
||||
@@ -127,26 +128,13 @@ class API:
|
||||
license=license,
|
||||
openapi_route=openapi_route,
|
||||
static_route=static_route,
|
||||
openapi_theme=openapi_theme,
|
||||
)
|
||||
|
||||
# TODO: Update docs for templates
|
||||
self.templates = Templates(directory=templates_dir)
|
||||
|
||||
if request_id:
|
||||
import uuid as _uuid
|
||||
|
||||
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()
|
||||
self.requests = (
|
||||
self.session()
|
||||
) #: A Requests session that is connected to the ASGI app.
|
||||
|
||||
@property
|
||||
def static_app(self):
|
||||
@@ -162,79 +150,12 @@ class API:
|
||||
|
||||
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):
|
||||
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):
|
||||
"""
|
||||
Decorator for creating new routes around function and class definitions.
|
||||
|
||||
"""Decorator for creating new routes around function and class definitions.
|
||||
Usage::
|
||||
|
||||
from marshmallow import Schema, fields
|
||||
@api.schema("Pet")
|
||||
class PetSchema(Schema):
|
||||
@@ -251,12 +172,11 @@ class API:
|
||||
"""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.
|
||||
""" # noqa: E501 (Line too long)
|
||||
"""
|
||||
for route in self.router.routes:
|
||||
match, _ = route.matches(path)
|
||||
if match:
|
||||
return route
|
||||
return None
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
@@ -268,18 +188,16 @@ class API:
|
||||
check_existing=True,
|
||||
websocket=False,
|
||||
before_request=False,
|
||||
methods=None,
|
||||
):
|
||||
"""Adds a route to the API.
|
||||
|
||||
:param route: A string representation of the route.
|
||||
: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 static: If ``True``, and no endpoint was passed, render "static/index.html".
|
||||
Also, it will become a default route.
|
||||
:param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).
|
||||
""" # noqa: E501
|
||||
:param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
|
||||
"""
|
||||
|
||||
# Path
|
||||
if static:
|
||||
assert self.static_dir is not None
|
||||
if not endpoint:
|
||||
@@ -293,35 +211,27 @@ class API:
|
||||
websocket=websocket,
|
||||
before_request=before_request,
|
||||
check_existing=check_existing,
|
||||
methods=methods,
|
||||
)
|
||||
|
||||
async def _static_response(self, req, resp):
|
||||
assert self.static_dir is not None
|
||||
|
||||
index = (self.static_dir / "index.html").resolve()
|
||||
if index.exists():
|
||||
resp.html = index.read_text()
|
||||
if os.path.exists(index):
|
||||
with open(index, "r") as f:
|
||||
resp.html = f.read()
|
||||
else:
|
||||
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined]
|
||||
resp.status_code = status_codes.HTTP_404
|
||||
resp.text = "Not found."
|
||||
|
||||
def redirect(
|
||||
self,
|
||||
resp,
|
||||
location,
|
||||
*,
|
||||
set_text=True,
|
||||
status_code=status_codes.HTTP_301, # type: ignore[attr-defined]
|
||||
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
|
||||
):
|
||||
"""
|
||||
Redirects a given response to a given location.
|
||||
|
||||
"""Redirects a given response to a given location.
|
||||
:param resp: The Response to mutate.
|
||||
:param location: The location of the redirect.
|
||||
:param set_text: If ``True``, sets the Redirect body content automatically.
|
||||
:param status_code: an `API.status_codes` attribute, or an integer,
|
||||
representing the HTTP status code of the redirect.
|
||||
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Usage::
|
||||
@@ -365,89 +275,34 @@ class API:
|
||||
def hello(req, resp):
|
||||
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):
|
||||
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)
|
||||
return f
|
||||
|
||||
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):
|
||||
"""Mounts an WSGI / ASGI application at a given route.
|
||||
|
||||
:param route: String representation of the route to be used
|
||||
(shouldn't be parameterized).
|
||||
:param route: String representation of the route to be used (shouldn't be parameterized).
|
||||
:param app: The other WSGI / ASGI app.
|
||||
"""
|
||||
self.router.apps.update({route: app})
|
||||
|
||||
def session(self, base_url="http://;"):
|
||||
"""Testing HTTP client. Returns a Starlette TestClient instance,
|
||||
able to send HTTP requests to the Responder application.
|
||||
"""Testing HTTP client. Returns a Requests session object, 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:
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
self._session = TestClient(self, base_url=base_url)
|
||||
return self._session
|
||||
|
||||
def url_for(self, endpoint, **params):
|
||||
# TODO: Absolute_url
|
||||
"""Given an endpoint, returns a rendered URL for its route.
|
||||
|
||||
:param endpoint: The route endpoint you're searching for.
|
||||
@@ -456,87 +311,53 @@ class API:
|
||||
return self.router.url_for(endpoint, **params)
|
||||
|
||||
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 \*args: Data to pass into the template.
|
||||
:param \*\*kwargs: Data to pass into the template.
|
||||
:param *args: Data to pass into the template.
|
||||
:param *kwargs: Date to pass into the template.
|
||||
"""
|
||||
return self.templates.render(filename, *args, **kwargs)
|
||||
|
||||
def template_string(self, source, *args, **kwargs):
|
||||
r"""Render a Jinja2 template string with the provided values.
|
||||
|
||||
:param source: The template to use, a Jinja2 template string.
|
||||
:param \*args: Data to pass into the template.
|
||||
:param \*\*kwargs: Data to pass into the template.
|
||||
"""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.
|
||||
:param *args: Data to pass into the template.
|
||||
:param **kwargs: Data to pass into the template.
|
||||
"""
|
||||
return self.templates.render_string(source, *args, **kwargs)
|
||||
|
||||
def serve(self, *, address=None, port=None, debug=False, **options):
|
||||
"""
|
||||
Run the application with uvicorn.
|
||||
|
||||
If the ``PORT`` environment variable is set, requests will be served on that port
|
||||
automatically to all known hosts.
|
||||
"""Runs the application with uvicorn. 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 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()``.
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
if "PORT" in os.environ:
|
||||
if address is None:
|
||||
address = "0.0.0.0" # noqa: S104
|
||||
address = "0.0.0.0"
|
||||
port = int(os.environ["PORT"])
|
||||
|
||||
if address is None:
|
||||
address = "127.0.0.1"
|
||||
if port is None:
|
||||
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):
|
||||
if "debug" not in kwargs:
|
||||
kwargs.update({"debug": self.debug})
|
||||
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):
|
||||
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
Reference in New Issue
Block a user