Compare commits

..

1 Commits

Author SHA1 Message Date
taoufik 8a46a87b3e Fix missing openapi title, version and openapi_version and black 2019-12-03 19:38:12 +01:00
110 changed files with 2157 additions and 7984 deletions
-3
View File
@@ -1,3 +0,0 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
-16
View File
@@ -1,16 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
-42
View File
@@ -1,42 +0,0 @@
name: "Documentation"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
documentation:
name: "Documentation"
runs-on: ubuntu-latest
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-dependency-glob: |
pyproject.toml
- name: Install package and documentation dependencies
run: uv pip install '.[docs]'
- name: Build static HTML documentation
run: sphinx-build -W --keep-going docs/source docs/build
-56
View File
@@ -1,56 +0,0 @@
name: "Tests"
on:
push:
branches: [ main ]
pull_request: ~
workflow_dispatch:
# Cancel redundant in-progress jobs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [
"3.9",
"3.10",
"3.11",
"3.12",
"3.13",
]
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
cache-suffix: ${{ matrix.python-version }}
cache-dependency-glob: |
pyproject.toml
- name: Install package
run: uv pip install '.[develop,test]'
- name: Run tests
run: pytest
-2
View File
@@ -1,4 +1,3 @@
.venv*
.vscode/ .vscode/
.cache .cache
.idea .idea
@@ -7,7 +6,6 @@
.pytest_cache .pytest_cache
.DS_Store .DS_Store
coverage.xml coverage.xml
.coverage*
__pycache__ __pycache__
tests/__pycache__ tests/__pycache__
-33
View File
@@ -1,33 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# Details
# - https://docs.readthedocs.io/en/stable/config-file/v2.html
# Required
version: 2
build:
os: "ubuntu-24.04"
tools:
python: "3.12"
python:
install:
- method: pip
path: .
extra_requirements:
- docs
sphinx:
configuration: docs/source/conf.py
# Use standard HTML builder.
builder: html
# Fail on all warnings to avoid broken references.
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF
#formats:
# - pdf
+18
View File
@@ -0,0 +1,18 @@
# travis use trusty by default
dist: xenial
language: python
python:
- 3.6
- 3.7
- "3.8-dev"
# command to install dependencies
install:
- pip install pipenv --upgrade-strategy=only-if-needed
- pipenv install --dev
# command to run the dependencies
script:
- black responder tests setup.py --check
- pytest
+47 -195
View File
@@ -1,422 +1,274 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [v3.0.0] - 2026-03-22
### Added
- Platform: Added support for Python 3.10 - Python 3.13
- CLI: `responder run` now also accepts a filesystem path on its `<target>`
argument, enabling usage on single-file applications.
- CLI: `responder run` now also accepts URLs.
### Changed
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
- Moved `apispec` and `marshmallow` to `openapi` optional extra
- Replaced `rfc3986` with stdlib `urllib.parse`
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
- Switched from WhiteNoise to ServeStatic
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the `responder.ext` module namespace.
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
### Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated `resume_incomplete` and `resume`
aliases for HTTP 308 (marked for removal in 3.0)
- CLI: `responder run --build` ceased to exist
### Fixed
- Routing: Fixed dispatching `static_route=None` on Windows
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
- Tests: Fixed deprecated httpx TestClient usage
## [v2.0.5] - 2019-12-15
### Added
- Update requirements to support python 3.8
## [v2.0.4] - 2019-11-19 ## [v2.0.4] - 2019-11-19
### Fixed ### Fixed
- Fix static app resolving - Fix static app resolving
## [v2.0.3] - 2019-09-20 ## [v2.0.3] - 2019-09-20
### Fixed ### Fixed
- Fix template conflicts - Fix template conflicts
## [v2.0.2] - 2019-09-20 ## [v2.0.2] - 2019-09-20
### Fixed ### Fixed
- Fix template conflicts - Fix template conflicts
## [v2.0.1] - 2019-09-20 ## [v2.0.1] - 2019-09-20
### Fixed ### Fixed
- Fix template import - Fix template import
## [v2.0.0] - 2019-09-19 ## [v2.0.0] - 2019-09-19
### Changed ### Changed
- Refactor Router and Schema - Refactor Router and Schema
## [v1.3.2] - 2019-08-15 ## [v1.3.2] - 2019-08-15
### Added ### Added
- ASGI 3 support - ASGI 3 support
- CI tests for python 3.8-dev - CI tests for python 3.8-dev
- Now requests have `state` a mapping object - Now requests have `state` a mapping object
### Deprecated ### Deprecated
- ASGI 2 - ASGI 2
## [v1.3.1] - 2019-04-28 ## [v1.3.1] - 2019-04-28
### Added ### Added
- Route params Converters - Route params Converters
- Add search for documentation pages - Add search for documentation pages
### Changed ### Changed
- Bump dependencies - Bump dependencies
## [v1.3.0] - 2019-02-22 ## [v1.3.0] - 2019-02-22
### Fixed ### Fixed
- Versioning issue - Versioning issue
- Multiple cookies. - Multiple cookies.
- Whitenoise returns not found. - Whitenoise returns not found.
- Other bugfixes. - Other bugfixes.
### Added ### Added
- Stream support via `resp.stream`. - Stream support via `resp.stream`.
- Cookie directives via `resp.set_cookie`. - Cookie directives via `resp.set_cookie`.
- Add `resp.html` to send HTML. - Add `resp.html` to send HTML.
- Other improvements. - Other improvements.
## [v1.1.3] - 2019-01-12 ## [v1.1.3] - 2019-01-12
### Changed ### Changed
- Refactor `_route_for` - Refactor `_route_for`
### Fixed ### Fixed
- Resolve startup/shutdwown events - Resolve startup/shutdwown events
## [v1.2.0] - 2018-12-29 ## [v1.2.0] - 2018-12-29
### Added ### Added
- Documentations - Documentations
### Changed ### Changed
- Use Starlette's LifeSpan middleware - Use Starlette's LifeSpan middleware
- Update denpendencies - Update denpendencies
### Fixed ### Fixed
- Fix route.is_class_based - Fix route.is_class_based
- Fix test_500 - Fix test_500
- Typos - Typos
## [v1.1.2] - 2018-11-11 ## [v1.1.2] - 2018-11-11
### Fixed ### Fixed
- Minor fixes for Open API - Minor fixes for Open API
- Typos - Typos
## [v1.1.1] - 2018-10-29 ## [v1.1.1] - 2018-10-29
### Changed ### Changed
- Run sync views in a threadpoolexecutor. - Run sync views in a threadpoolexecutor.
## [v1.1.0] - 2018-10-27 ## [v1.1.0] - 2018-10-27
### Added ### Added
- Support for `before_request`. - Support for `before_request`.
## [v1.0.5]- 2018-10-27 ## [v1.0.5]- 2018-10-27
### Fixed ### Fixed
- Fix sessions. - Fix sessions.
## [v1.0.4] - 2018-10-27 ## [v1.0.4] - 2018-10-27
### Fixed ### Fixed
- Potential bufix for cookies. - Potential bufix for cookies.
## [v1.0.3] - 2018-10-27 ## [v1.0.3] - 2018-10-27
### Fixed ### Fixed
- Bugfix for redirects. - Bugfix for redirects.
## [v1.0.2] - 2018-10-27 ## [v1.0.2] - 2018-10-27
### Changed ### Changed
- Improvement for static file hosting. - Improvement for static file hosting.
## [v1.0.1] - 2018-10-26 ## [v1.0.1] - 2018-10-26
### Changed ### Changed
- Improve cors configuration settings. - Improve cors configuration settings.
## [v1.0.0] - 2018-10-26 ## [v1.0.0] - 2018-10-26
### Changed ### Changed
- Move GraphQL support into a built-in plugin. - Move GraphQL support into a built-in plugin.
## [v0.3.3] - 2018-10-25 ## [v0.3.3] - 2018-10-25
### Added ### Added
- CORS support - CORS support
### Changed ### Changed
- Improved exceptions. - Improved exceptions.
## [v0.3.2] - 2018-10-25 ## [v0.3.2] - 2018-10-25
### Changed ### Changed
- Subtle improvements. - Subtle improvements.
## [v0.3.1] - 2018-10-24 ## [v0.3.1] - 2018-10-24
### Fixed ### Fixed
- Packaging fix. - Packaging fix.
## [v0.3.0] - 2018-10-24 ## [v0.3.0] - 2018-10-24
### Changed ### Changed
- Interactive Documentation endpoint. - Interactive Documentation endpoint.
- Minor improvements. - Minor improvements.
## [v0.2.3] - 2018-10-24 ## [v0.2.3] - 2018-10-24
### Changed ### Changed
- Overall improvements. - Overall improvements.
## [v0.2.2] - 2018-10-23 ## [v0.2.2] - 2018-10-23
### Added ### Added
- Show traceback info when background tasks raise exceptions. - Show traceback info when background tasks raise exceptions.
## [v0.2.1] - 2018-10-23 ## [v0.2.1] - 2018-10-23
### Added ### Added
- api.requests. - api.requests.
## [v0.2.0] - 2018-10-22 ## [v0.2.0] - 2018-10-22
### Added ### Added
- WebSocket support. - WebSocket support.
## [v0.1.6] - 2018-10-20 ## [v0.1.6] - 2018-10-20
### Added ### Added
- 500 support. - 500 support.
## [v0.1.5] - 2018-10-20 ## [v0.1.5] - 2018-10-20
### Added ### Added
- File upload support - File upload support
### Changed ### Changed
- Improvements to sequential media reading. - Improvements to sequential media reading.
## [v0.1.4] - 2018-10-19 ## [v0.1.4] - 2018-10-19
### Fixed ### Fixed
- Stability. - Stability.
## [v0.1.3] - 2018-10-18 ## [v0.1.3] - 2018-10-18
### Added ### Added
- Sessions support. - Sessions support.
## [v0.1.2] - 2018-10-18 ## [v0.1.2] - 2018-10-18
### Added ### Added
- Cookies support. - Cookies support.
## [v0.1.1] - 2018-10-17 ## [v0.1.1] - 2018-10-17
### Changed ### Changed
- Default routes. - Default routes.
## [v0.1.0] - 2018-10-17 ## [v0.1.0] - 2018-10-17
### Added ### Added
- Prototype of static application support. - Prototype of static application support.
## [v0.0.10] - 2018-10-17 ## [v0.0.10] - 2018-10-17
### Fixed ### Fixed
- Bugfix for async class-based views. - Bugfix for async class-based views.
## [v0.0.9] - 2018-10-17 ## [v0.0.9] - 2018-10-17
### Fixed ### Fixed
- Bugfix for async class-based views. - Bugfix for async class-based views.
## [v0.0.8] - 2018-10-17 ## [v0.0.8] - 2018-10-17
### Added ### Added
- GraphiQL Support. - GraphiQL Support.
### Changed ### Changed
- Improvement to route selection. - Improvement to route selection.
## [v0.0.7] - 2018-10-16 ## [v0.0.7] - 2018-10-16
### Changed ### Changed
- Immutable Request object. - Immutable Request object.
## [v0.0.6] - 2018-10-16 ## [v0.0.6] - 2018-10-16
### Added ### Added
- Ability to mount WSGI apps. - Ability to mount WSGI apps.
- Supply content-type when serving up the schema. - Supply content-type when serving up the schema.
## [v0.0.5] - 2018-10-15 ## [v0.0.5] - 2018-10-15
### Added ### Added
- OpenAPI Schema support. - OpenAPI Schema support.
- Safe load/dump yaml. - Safe load/dump yaml.
## [v0.0.4] - 2018-10-15 ## [v0.0.4] - 2018-10-15
### Added ### Added
- Asynchronous support for data uploads. - Asynchronous support for data uploads.
### Fixed ### Fixed
- Bug fixes. - Bug fixes.
## [v0.0.3] - 2018-10-13 ## [v0.0.3] - 2018-10-13
### Fixed ### Fixed
- Bug fixes. - Bug fixes.
## [v0.0.2] - 2018-10-13 ## [v0.0.2] - 2018-10-13
### Changed ### Changed
- Switch to ASGI/Starlette. - Switch to ASGI/Starlette.
## [v0.0.1] - 2018-10-12 ## [v0.0.1] - 2018-10-12
### Added ### Added
- Conception! - Conception!
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.0.0..HEAD [Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.4..HEAD
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0 [v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5 [v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4 [v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3 [v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2 [v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1 [v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0 [v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2 [v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1 [v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0 [v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0 [v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3 [v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2 [v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1 [v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0 [v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5 [v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4 [v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3 [v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2 [v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1 [v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0 [v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3 [v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2 [v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1 [v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0 [v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3 [v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2 [v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1 [v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0 [v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6 [v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5 [v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4 [v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3 [v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2 [v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1 [v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0 [v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10 [v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9 [v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8 [v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7 [v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6 [v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5 [v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4 [v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3 [v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
+10 -175
View File
@@ -1,178 +1,13 @@
Copyright 2018 Kenneth Reitz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Apache License http://www.apache.org/licenses/LICENSE-2.0
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
1. Definitions. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
"License" shall mean the terms and conditions for use, reproduction, limitations under the License.
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+1
View File
@@ -0,0 +1 @@
include LICENSE
+20
View File
@@ -0,0 +1,20 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = {editable = true, path = "."}
[dev-packages]
pytest = "*"
"flake8" = "*"
black = "*"
twine = "*"
flask = "*"
sphinx = "*"
marshmallow = "*"
pytest-cov = "*"
[pipenv]
allow_prereleases = true
Generated
+762
View File
@@ -0,0 +1,762 @@
{
"_meta": {
"hash": {
"sha256": "ea12c0d556a3ca0848b0eba291a11a5ea98a701f0885c2d030b2aeb1e5b9c15f"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"version": "==0.4.0"
},
"aniso8601": {
"hashes": [
"sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e",
"sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"
],
"version": "==7.0.0"
},
"apispec": {
"hashes": [
"sha256:5fdaa1173b32515cc83f9d413a49a6c37fafc2b87f6b40e95923d3e85f0942c5",
"sha256:9e88c51517a6515612e818459f61c1bc06c00f2313e5187828bdbabaa7461473"
],
"version": "==3.0.0"
},
"apistar": {
"hashes": [
"sha256:8da0d3f15748c8ed6e68914ba5b8f6dd5dff5afbe137950d07103575df0bce73"
],
"version": "==0.7.2"
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"graphene": {
"hashes": [
"sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896",
"sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"
],
"version": "==2.1.8"
},
"graphql-core": {
"hashes": [
"sha256:1488f2a5c2272dc9ba66e3042a6d1c30cea0db4c80bd1e911c6791ad6187d91b",
"sha256:da64c472d720da4537a2e8de8ba859210b62841bd47a9be65ca35177f62fe0e4"
],
"version": "==2.2.1"
},
"graphql-relay": {
"hashes": [
"sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335",
"sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5",
"sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6"
],
"version": "==2.0.0"
},
"graphql-server-core": {
"hashes": [
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
],
"version": "==1.1.1"
},
"h11": {
"hashes": [
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
],
"version": "==0.8.1"
},
"httptools": {
"hashes": [
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
],
"version": "==0.0.13"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
],
"version": "==2.10.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
],
"version": "==3.2.1"
},
"promise": {
"hashes": [
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"version": "==2.2.1"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"version": "==0.0.5"
},
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
],
"version": "==5.1.2"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"responder": {
"editable": true,
"path": "."
},
"rfc3986": {
"hashes": [
"sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405",
"sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"
],
"version": "==1.3.2"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
],
"version": "==1.6.1"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"starlette": {
"hashes": [
"sha256:e41ef52e711a82ef95c195674e5d8d41c75c6b1d6f5a275637eedd4cc2150a7f"
],
"version": "==0.12.10"
},
"typesystem": {
"hashes": [
"sha256:ba2bd10f1c5844d08dd8841e777bdee55bfca569bf21cb96cd0f91e0a4f66cd8"
],
"version": "==0.2.4"
},
"urllib3": {
"hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"version": "==1.25.6"
},
"uvicorn": {
"hashes": [
"sha256:8aa44f9d9c3082ef693950387ea25d376e32944df6d4071dbd8edc3c25a40c74"
],
"version": "==0.8.6"
},
"uvloop": {
"hashes": [
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
],
"version": "==0.12.2"
},
"websockets": {
"hashes": [
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
],
"version": "==7.0"
},
"whitenoise": {
"hashes": [
"sha256:22f79cf8f1f509639330f93886acaece8ec5ac5e9600c3b981d33c34e8a42dfd",
"sha256:6dfea214b7c12efd689007abf9afa87a426586e9dbc051873ad2c8e535e2a1ac"
],
"version": "==4.1.4"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.3.0"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"babel": {
"hashes": [
"sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
"sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
],
"version": "==2.7.0"
},
"black": {
"hashes": [
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==19.3b0"
},
"bleach": {
"hashes": [
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"version": "==3.1.0"
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"coverage": {
"hashes": [
"sha256:17a417c691de3fc88de027832267313e5ed2b2ea3956745b562c4c389e44d05b",
"sha256:24307e67ebd9dc06fcbab9b7fef87412a97746c1baabb04ed8a93d5c2ccfe5ba",
"sha256:2a5d44a9d8426bd3699123864e63f008dc8dea9df22d5216a141a25d4670f22c",
"sha256:3726b8f5461e103a40e380f52b4b4ccdf2eda55d5d72f037cee43627992b4462",
"sha256:39dd15bbc4880a64399e180925bbc21c0c316a3065f6455d2512039f5cb59b94",
"sha256:3bb121f5dd156aab4fba2ebad6b0ad605bc5dc305931140dc614b101aa9d81ed",
"sha256:3bfdea9226eaed97736c973a7d6d0bbf9e1c1f1c7391c8e9c2bb2d0dbae49156",
"sha256:43be906a16239c1aa9f3742e3e6b0a5dd24781a13ce401f063262e9b4e93b69f",
"sha256:4a54cac1b39b2925041a41bcd1f191898fe401618627d7c3abf127c32a1c6dd1",
"sha256:4e58d65b90d6f26b3ccca7cf0fe573ef847347b8734af596a087a21eebb681f5",
"sha256:50229727d9baf0cd7f5ee6b194bf9dea708e9a20823d93f9e04d710b0a60e757",
"sha256:5141cdb010e9cd6939e37b8c2769d535cb535d80ef94f927c8a306f2e05a4736",
"sha256:748ba2b950425b9aef9d1bde2d6af7023585505016bd634e578f76ada4a30465",
"sha256:75e635bc6730c88b04421b25a0afc47b9b80efc1ed57630839196eb475722e50",
"sha256:78556f51dbfb33f18794eee29a4a8542fd2e301aa0d072653930793974dced03",
"sha256:7de17133509210ecc256535bab2f9a5547f3016c44f984fe12b4c10d81a4623f",
"sha256:83bf376555898fe2dc50d111a34b0152b504e454ed1e13cdcda6e5d50ba0ed5b",
"sha256:87730b5e4c3a42674fe8f0ecbb0d556c59c7e12b11a65c2178f2787252a80dfd",
"sha256:9bb7819c020c20c6200764879f0b10b323d6d4719aa7b0ae316c9e35730f9e2d",
"sha256:9c825788acb13d49ac20455433f3b862029aa497e97faba8c998555a042a6b91",
"sha256:b2bb4941c8838fc9ea2fca3c52e6dd865d39bbbc014bde249161bf8fcccf2152",
"sha256:c1b44c6c680f137910cb0f5481a2ae9899787ca7019f110a3708d9e99df941be",
"sha256:c52c2bc67bd3ff8db685f7c5f03e34a95bddd58a535630161f28d1c485d61e22",
"sha256:d6845e46338695c571759be1c770b013c477111e785b26151ec9feb6cd063543",
"sha256:e292b32dfc80d9f271af2d52df95455248322156e764763c4bfb2385b2e33533"
],
"version": "==5.0a8"
},
"docutils": {
"hashes": [
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
],
"version": "==0.15.2"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"flake8": {
"hashes": [
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
],
"index": "pypi",
"version": "==3.7.8"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.1.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
"markers": "python_version < '3.8'",
"version": "==0.23"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
],
"version": "==2.10.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
],
"version": "==3.2.1"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
],
"version": "==7.2.0"
},
"packaging": {
"hashes": [
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
],
"version": "==19.2"
},
"pkginfo": {
"hashes": [
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
],
"version": "==1.5.0.1"
},
"pluggy": {
"hashes": [
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
],
"version": "==0.13.0"
},
"py": {
"hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
],
"version": "==1.8.0"
},
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==2.1.1"
},
"pygments": {
"hashes": [
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
],
"version": "==2.4.2"
},
"pyparsing": {
"hashes": [
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
],
"version": "==2.4.2"
},
"pytest": {
"hashes": [
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
],
"index": "pypi",
"version": "==5.2.1"
},
"pytest-cov": {
"hashes": [
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
],
"index": "pypi",
"version": "==2.8.1"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"readme-renderer": {
"hashes": [
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
],
"version": "==24.0"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
],
"version": "==2.0.0"
},
"sphinx": {
"hashes": [
"sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845",
"sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"
],
"index": "pypi",
"version": "==2.2.0"
},
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
],
"version": "==1.0.1"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
],
"version": "==1.0.1"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
],
"version": "==1.0.2"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
],
"version": "==1.0.2"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
],
"version": "==1.1.3"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"tqdm": {
"hashes": [
"sha256:abc25d0ce2397d070ef07d8c7e706aede7920da163c64997585d42d3537ece3d",
"sha256:dd3fcca8488bb1d416aa7469d2f277902f26260c45aa86b667b074cd44b3b115"
],
"version": "==4.36.1"
},
"twine": {
"hashes": [
"sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1",
"sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993"
],
"index": "pypi",
"version": "==2.0.0"
},
"urllib3": {
"hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"version": "==1.25.6"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"werkzeug": {
"hashes": [
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
],
"version": "==0.16.0"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
}
}
}
+44 -83
View File
@@ -1,109 +1,70 @@
# Responder # Responder: a familiar HTTP Service Framework for Python
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/). [![Build Status](https://travis-ci.org/taoufik07/responder.svg?branch=master)](https://travis-ci.org/taoufik07/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/taoufik07/responder.svg)](https://github.com/taoufik07/responder/graphs/contributors)
```python [![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
import responder
api = responder.API()
@api.route("/{greeting}") Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__": This gets you a ASGI app, with a production static files server pre-installed, jinja2 templating (without additional imports), and a production webserver based on uvloop, serving up requests with gzip compression automatically.
api.run()
```
$ pip install responder
That's it. Supports Python 3.9+. ## Testimonials
## The Basics > "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
- `resp.text` sends back text. `resp.html` sends back HTML. `resp.content` sends back bytes. > "ASGI is going to enable all sorts of new high-performance web services. It's awesome to see Responder starting to take advantage of that." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
- `resp.media` sends back JSON (or YAML, with content negotiation).
- `resp.file("path.pdf")` serves a file with automatic content-type detection.
- `req.headers` is case-insensitive. `req.params` gives you query parameters.
- Both sync and async views work — the `async` is optional.
## Highlights > "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
```python
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
# HTTP method filtering ## More Examples
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
# Class-based views See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# Custom error handling # Installing Responder
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
# Lifespan events Install the stable release:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
api = responder.API(lifespan=lifespan) $ pipenv install responder
✨🍰✨
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
# WebSockets Or, install from the development branch:
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
# Mount WSGI/ASGI apps $ pipenv install -e git+https://github.com/taoufik07/responder.git#egg=responder
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
# Background tasks Only **Python 3.6+** is supported.
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
```
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: `str`, `int`, `float`, `uuid`, `path`. # The Basic Idea
## Documentation The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
https://responder.kennethreitz.org - Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## Ideas
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests.
- A production static file server is built-in.
- Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
- Provide an official way to run webpack.
+48
View File
@@ -0,0 +1,48 @@
alabaster==0.7.12
appdirs==1.4.3
atomicwrites==1.2.1
attrs==18.2.0
babel==2.6.0
black==18.9b0
bleach==3.0.2
certifi==2018.8.24
cffi==1.11.5
chardet==3.0.4
click==7.0
cmarkgfm==0.4.2
colorama==0.4.0 ; sys_platform == 'win32'
docutils==0.14
flake8==3.5.0
flask==1.0.2
future==0.16.0
idna==2.7
imagesize==1.1.0
itsdangerous==0.24
jinja2==2.10
markupsafe==1.0
mccabe==0.6.1
more-itertools==4.3.0
packaging==18.0
pkginfo==1.4.2
pluggy==0.7.1
py==1.7.0
pycodestyle==2.3.1
pycparser==2.19
pyflakes==1.6.0
pygments==2.2.0
pyparsing==2.2.2
pytest==3.8.2
pytz==2018.5
readme-renderer==22.0
requests-toolbelt==0.8.0
requests==2.19.1
six==1.11.0
snowballstemmer==1.2.1
sphinx==1.8.1
sphinxcontrib-websupport==1.1.0
toml==0.10.0
tqdm==4.26.0
twine==1.12.1
urllib3==1.23
webencodings==0.5.1
werkzeug==0.15.5
+4 -4
View File
@@ -1,7 +1,7 @@
/* Hide module name and default value for environment variable section */ /* Hide module name and default value for environment variable section */
div[id$="environment-variables"] code.descclassname { div[id$='environment-variables'] code.descclassname {
display: none; display: none;
} }
div[id$="environment-variables"] em.property { div[id$='environment-variables'] em.property {
display: none; display: none;
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+131 -141
View File
@@ -10,152 +10,142 @@
*/ */
var Konami = function (callback) { var Konami = function (callback) {
var konami = { var konami = {
addEvent: function (obj, type, fn, ref_obj) { addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener) obj.addEventListener(type, fn, false); if (obj.addEventListener)
else if (obj.attachEvent) { obj.addEventListener(type, fn, false);
// IE else if (obj.attachEvent) {
obj["e" + type + fn] = fn; // IE
obj[type + fn] = function () { obj["e" + type + fn] = fn;
obj["e" + type + fn](window.event, ref_obj); obj[type + fn] = function () {
}; obj["e" + type + fn](window.event, ref_obj);
obj.attachEvent("on" + type, obj[type + fn]); }
} obj.attachEvent("on" + type, obj[type + fn]);
}, }
removeEvent: function (obj, eventName, eventCallback) { },
if (obj.removeEventListener) { removeEvent: function (obj, eventName, eventCallback) {
obj.removeEventListener(eventName, eventCallback); if (obj.removeEventListener) {
} else if (obj.attachEvent) { obj.removeEventListener(eventName, eventCallback);
obj.detachEvent(eventName); } else if (obj.attachEvent) {
} obj.detachEvent(eventName);
}, }
input: "", },
pattern: "38384040373937396665", input: "",
keydownHandler: function (e, ref_obj) { pattern: "38384040373937396665",
if (ref_obj) { keydownHandler: function (e, ref_obj) {
konami = ref_obj; if (ref_obj) {
} // IE konami = ref_obj;
konami.input += e ? e.keyCode : event.keyCode; } // IE
if (konami.input.length > konami.pattern.length) { konami.input += e ? e.keyCode : event.keyCode;
konami.input = konami.input.substr(konami.input.length - konami.pattern.length); 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); if (konami.input === konami.pattern) {
konami.input = ""; konami.code(konami._currentLink);
e.preventDefault(); konami.input = '';
return false; e.preventDefault();
} return false;
}, }
load: function (link) { },
this._currentLink = link; load: function (link) {
this.addEvent(document, "keydown", this.keydownHandler, this); this._currentLink = link;
this.iphone.load(link); this.addEvent(document, "keydown", this.keydownHandler, this);
}, this.iphone.load(link);
unload: function () { },
this.removeEvent(document, "keydown", this.keydownHandler); unload: function () {
this.iphone.unload(); this.removeEvent(document, 'keydown', this.keydownHandler);
}, this.iphone.unload();
code: function (link) { },
window.location = link; code: function (link) {
}, window.location = link
iphone: { },
start_x: 0, iphone: {
start_y: 0, start_x: 0,
stop_x: 0, start_y: 0,
stop_y: 0, stop_x: 0,
tap: false, stop_y: 0,
capture: false, tap: false,
orig_keys: "", capture: false,
keys: [ orig_keys: "",
"UP", keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
"UP", input: [],
"DOWN", code: function (link) {
"DOWN", konami.code(link);
"LEFT", },
"RIGHT", touchmoveHandler: function (e) {
"LEFT", if (e.touches.length === 1 && konami.iphone.capture === true) {
"RIGHT", var touch = e.touches[0];
"TAP", konami.iphone.stop_x = touch.pageX;
"TAP", konami.iphone.stop_y = touch.pageY;
], konami.iphone.tap = false;
input: [], konami.iphone.capture = false;
code: function (link) { konami.iphone.check_direction();
konami.code(link); }
}, },
touchmoveHandler: function (e) { touchendHandler: function () {
if (e.touches.length === 1 && konami.iphone.capture === true) { konami.iphone.input.push(konami.iphone.check_direction());
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX; if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false; if (konami.iphone.input.length === konami.iphone.keys.length) {
konami.iphone.capture = false; var match = true;
konami.iphone.check_direction(); for (var i = 0; i < konami.iphone.keys.length; i++) {
} if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
}, match = false;
touchendHandler: function () { }
konami.iphone.input.push(konami.iphone.check_direction()); }
if (match) {
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.code(konami._currentLink);
konami.iphone.input.shift(); }
}
if (konami.iphone.input.length === konami.iphone.keys.length) { },
var match = true; touchstartHandler: function (e) {
for (var i = 0; i < konami.iphone.keys.length; i++) { konami.iphone.start_x = e.changedTouches[0].pageX;
if (konami.iphone.input[i] !== konami.iphone.keys[i]) { konami.iphone.start_y = e.changedTouches[0].pageY;
match = false; 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;
} }
}
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); typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") { if (typeof callback === "function") {
konami.code = callback; konami.code = callback;
konami.load(); konami.load();
} }
return konami; return konami;
}; };
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = Konami; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Konami;
} else { } else {
if (typeof define === "function" && define.amd) { if (typeof define === 'function' && define.amd) {
define([], function () { define([], function() {
return Konami; return Konami;
}); });
} else { } else {
window.Konami = Konami; window.Konami = Konami;
} }
} }
+37 -73
View File
@@ -1,13 +1,5 @@
<link <link rel="stylesheet" type="text/css" href="https://cloud.typography.com/7584432/7586812/css/fonts.css" />
rel="stylesheet" <script type="text/javascript">$('#searchbox').hide(0);</script>
type="text/css"
href="https://cloud.typography.com/7584432/7586812/css/fonts.css"
/>
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script type="text/javascript">
$("#searchbox").hide(0);
</script>
<!--Alabaster (krTheme++) Hacks --> <!--Alabaster (krTheme++) Hacks -->
<!-- CSS Adjustments (I'm very picky.) --> <!-- CSS Adjustments (I'm very picky.) -->
@@ -47,7 +39,9 @@
} }
.method { .method {
margin-bottom: 2em; margin-bottom: 2em;
} }
.si, .si,
@@ -86,6 +80,8 @@
margin-top: -1em; margin-top: -1em;
} }
/* "Quick Search" should be not be shown for now. */ /* "Quick Search" should be not be shown for now. */
div#searchbox h3 { div#searchbox h3 {
display: none; display: none;
@@ -122,12 +118,10 @@
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag() { function gtag() { dataLayer.push(arguments); }
dataLayer.push(arguments); gtag('js', new Date());
}
gtag("js", new Date());
gtag("config", "UA-127383416-1"); gtag('config', 'UA-127383416-1');
</script> </script>
<!-- There are no more hacks. --> <!-- There are no more hacks. -->
@@ -136,10 +130,7 @@
<script src="{{ pathto('_static/', 1) }}/konami.js"></script> <script src="{{ pathto('_static/', 1) }}/konami.js"></script>
<script> <script>
var easter_egg = new Konami( var easter_egg = new Konami('https://www.myfortunecookie.co.uk/fortunes/' + (Math.floor(Math.random() * 152) + 1));
"https://www.myfortunecookie.co.uk/fortunes/" +
(Math.floor(Math.random() * 152) + 1)
);
</script> </script>
<style> <style>
@@ -149,94 +140,67 @@
</style> </style>
<!-- GitHub Logo --> <!-- GitHub Logo -->
<a <a href="https://github.com/kennethreitz/responder" class="github-corner" aria-label="View source on GitHub">
href="https://github.com/kennethreitz/responder" <svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
class="github-corner" aria-hidden="true">
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="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<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"
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>
fill="currentColor" <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"
style="transform-origin: 130px 106px" fill="currentColor" class="octo-body"></path>
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> </svg>
</a> </a>
<style> <style>
.github-corner:hover .octo-arm { .github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out; animation: octocat-wave 560ms ease-in-out
} }
@keyframes octocat-wave { @keyframes octocat-wave {
0%, 0%,
100% { 100% {
transform: rotate(0); transform: rotate(0)
} }
20%, 20%,
60% { 60% {
transform: rotate(-25deg); transform: rotate(-25deg)
} }
40%, 40%,
80% { 80% {
transform: rotate(10deg); transform: rotate(10deg)
} }
} }
@media (max-width: 500px) { @media (max-width:500px) {
.github-corner:hover .octo-arm { .github-corner:hover .octo-arm {
animation: none; animation: none
} }
.github-corner .octo-arm { .github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out; animation: octocat-wave 560ms ease-in-out
} }
} }
</style> </style>
<!-- That was not a hack. That was art. <!-- That was not a hack. That was art.
<!-- UserVoice JavaScript SDK (only needed once on a page) --> <!-- UserVoice JavaScript SDK (only needed once on a page) -->
<script> <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>
(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 --> <!-- A tab to launch the Classic Widget -->
<script> <script>
UserVoice = window.UserVoice || []; UserVoice = window.UserVoice || [];
UserVoice.push([ UserVoice.push(['showTab', 'classic_widget', {
"showTab", mode: 'feedback',
"classic_widget", primary_color: '#fa8c28',
{ link_color: '#0a8cc6',
mode: "feedback", forum_id: 913660,
primary_color: "#fa8c28", tab_label: 'Got feedback?',
link_color: "#0a8cc6", tab_color: '#00994f',
forum_id: 913660, tab_position: 'bottom-left',
tab_label: "Got feedback?", tab_inverted: true
tab_color: "#00994f", }]);
tab_position: "bottom-left",
tab_inverted: true,
},
]);
</script> </script>
+29 -63
View File
@@ -1,88 +1,54 @@
<p class="logo"> <p class="logo">
<a href="{{ pathto(master_doc) }}"> <a href="{{ pathto(master_doc) }}">
<img <img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
</a> </a>
</p> </p>
<p> <p>
<a class="github-button" <iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
href="https://github.com/kennethreitz/responder" allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
data-color-scheme="no-preference: light; light: light; dark: light;"
data-size="large"
data-show-count="true"
aria-label="Star kennethreitz/responder on GitHub">Star</a>
</p> </p>
<link <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style> <style>
.algolia-autocomplete { .algolia-autocomplete{
width: 100%; width: 100%;
height: 1.5em; height: 1.5em
} }
.algolia-autocomplete a { .algolia-autocomplete a{
border-bottom: none !important; border-bottom: none !important;
} }
#doc_search { #doc_search{
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>
<input id="doc_search" placeholder="Search the doc" autofocus /> <input id="doc_search" placeholder="Search the doc" autofocus/>
<script <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b', apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder', indexName: 'python-responder',
inputSelector: '#doc_search', inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown debug: false // Set debug to true if you want to inspect the dropdown
})" })" async></script>
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p> <p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<h3>Stay Informed</h3> <h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p> <p>Receive updates on new releases and upcoming projects.</p>
<p> <p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
<a class="github-button" frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
href="https://github.com/kennethreitz"
data-color-scheme="no-preference: light; light: light; dark: light;" <p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
data-size="medium" @kennethreitz</a>
data-show-count="true" <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>
aria-label="Follow @kennethreitz on GitHub">Follow @kennethreitz</a>
</p> </p>
<p>
<a
href="https://x.com/kennethreitz42"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3> <h3>Useful Links</h3>
<ul> <ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</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://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li> <li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
+28 -67
View File
@@ -1,93 +1,54 @@
<p class="logo"> <p class="logo">
<a href="{{ pathto(master_doc) }}"> <a href="{{ pathto(master_doc) }}">
<img <img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
class="logo"
src="{{ pathto('_static/responder.png', 1) }}"
title="https://kennethreitz.org/tattoos"
/>
</a> </a>
</p> </p>
<p> <p>
<iframe <iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
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>
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200px"
height="35px"
></iframe>
</p> </p>
<link <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style> <style>
.algolia-autocomplete { .algolia-autocomplete{
width: 100%; width: 100%;
height: 1.5em; height: 1.5em
} }
.algolia-autocomplete a { .algolia-autocomplete a{
border-bottom: none !important; border-bottom: none !important;
} }
#doc_search { #doc_search{
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>
<input id="doc_search" placeholder="Search the doc" autofocus /> <input id="doc_search" placeholder="Search the doc" autofocus/>
<script <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b', apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder', indexName: 'python-responder',
inputSelector: '#doc_search', inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown debug: false // Set debug to true if you want to inspect the dropdown
})" })" async></script>
async
></script>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p> <p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<h3>Stay Informed</h3> <h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p> <p>Receive updates on new releases and upcoming projects.</p>
<p> <p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
<iframe frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
allowtransparency="true" <p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
frameborder="0" @kennethreitz</a>
scrolling="0" <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>
width="200"
height="20"
></iframe>
</p> </p>
<p>
<a
href="https://x.com/kennethreitz42"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3> <h3>Useful Links</h3>
<ul> <ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</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://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li> <li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
-7
View File
@@ -1,7 +0,0 @@
# Backlog
## Future Ideas
- Consider adding `after_request` hooks (complement to `before_request`)
- Explore WebSocket before_request short-circuit support
- Add rate limiting middleware
- Consider async template rendering by default
-1
View File
@@ -1 +0,0 @@
../../CHANGELOG.md
-174
View File
@@ -1,174 +0,0 @@
Responder CLI
=============
Responder installs a command line program ``responder``. Use it to launch
a Responder application from a file or module, either located on a local
or remote filesystem, or object store.
Launch Module Entrypoint
------------------------
For loading a Responder application from a Python module, you will refer to
its ``API()`` instance using a `Python entry point object reference`_ that
points to a Python object. It is either in the form ``importable.module``,
or ``importable.module:object.attr``.
A basic invocation command to launch a Responder application:
.. code-block:: shell
responder run acme.app
The command above assumes a Python package ``acme`` including an ``app``
module ``acme/app.py`` that includes an attribute ``api`` that refers
to a ``responder.API`` instance, reflecting the typical layout of
a standard Responder application.
Loading a Responder application using an entrypoint specification will
inherit the capacities of `Python's import system`_, as implemented by
`importlib`_.
Launch Local File
-----------------
Acquire a minimal example single-file application, ``helloworld.py`` [1]_,
to your local filesystem, giving you the chance to edit it, and launch the
Responder HTTP service.
.. code-block:: shell
wget https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
responder run helloworld.py
.. note::
To validate the example application, invoke a HTTP request, for example using
`curl`_, `HTTPie`_, or your favourite browser at hand.
.. code-block:: shell
http http://127.0.0.1:5042/Hello
The response is no surprise.
::
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain
date: Sat, 26 Oct 2024 13:16:55 GMT
encoding: utf-8
server: uvicorn
Hello, world!
.. [1] The Responder application `helloworld.py`_ implements a basic echo handler.
Launch Remote File
------------------
You can also launch a single-file application where its Python file is stored
on a remote location.
Responder supports all filesystem adapters compatible with `fsspec`_, and
installs the adapters for Azure Blob Storage (az), Google Cloud Storage (gs),
GitHub, HTTP, and AWS S3 by default.
.. code-block:: shell
# Works 1:1.
responder run https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py
responder run github://kennethreitz:responder@/examples/helloworld.py
If you need access other kinds of remote targets, see the `list of
fsspec-supported filesystems and protocols`_. The next section enumerates
a few synthetic examples. The corresponding storage buckets do not even
exist, so don't expect those commands to work.
.. code-block:: shell
# Azure Blob Storage, Google Cloud Storage, and AWS S3.
responder run az://kennethreitz-assets/responder/examples/helloworld.py
responder run gs://kennethreitz-assets/responder/examples/helloworld.py
responder run s3://kennethreitz-assets/responder/examples/helloworld.py
# Hadoop Distributed File System (hdfs), SSH File Transfer Protocol (sftp),
# Common Internet File System (smb), Web-based Distributed Authoring and
# Versioning (webdav).
responder run hdfs://kennethreitz-assets/responder/examples/helloworld.py
responder run sftp://user@host/kennethreitz/responder/examples/helloworld.py
responder run smb://workgroup;user:password@server:port/responder/examples/helloworld.py
responder run webdav+https://user:password@server:port/responder/examples/helloworld.py
.. tip::
In order to install support for all filesystem types supported by fsspec, run:
.. code-block:: shell
uv pip install 'fsspec[full]'
When using ``uv``, this concludes within an acceptable time of approx.
25 seconds. If you need to be more selectively instead of using ``full``,
choose from one or multiple of the available `fsspec extras`_, which are:
abfs, arrow, dask, dropbox, fuse, gcs, git, github, hdfs, http, oci, s3,
sftp, smb, ssh.
Launch with Non-Standard Instance Name
--------------------------------------
By default, Responder will acquire an ``responder.API`` instance using the
symbol name ``api`` from the specified Python module.
If your main application file uses a different name than ``api``, please
append the designated symbol name to the launch target address.
It works like this for module entrypoints and local files:
.. code-block:: shell
responder run acme.app:service
responder run /path/to/acme/app.py:service
It works like this for URLs:
.. code-block:: shell
responder run http://app.server.local/path/to/acme/app.py#service
Within your ``app.py``, the instance would have been defined to use
the ``service`` symbol name instead of ``api``, like this:
.. code-block:: python
service = responder.API()
Build JavaScript Application
----------------------------
The ``build`` subcommand invokes ``npm run build``, optionally accepting
a target directory. By default, it uses the current working directory,
where it expects a regular NPM ``package.json`` file.
.. code-block:: shell
responder build
When specifying a target directory, Responder will change to that
directory beforehand.
.. code-block:: shell
responder build /path/to/project
.. _curl: https://curl.se/
.. _fsspec: https://filesystem-spec.readthedocs.io/en/latest/
.. _fsspec extras: https://github.com/fsspec/filesystem_spec/blob/2024.12.0/pyproject.toml#L27-L69
.. _helloworld.py: https://github.com/kennethreitz/responder/blob/main/examples/helloworld.py
.. _HTTPie: https://httpie.io/docs/cli
.. _importlib: https://docs.python.org/3/library/importlib.html
.. _list of fsspec-supported filesystems and protocols: https://github.com/fsspec/universal_pathlib#currently-supported-filesystems-and-protocols
.. _Python entry point object reference: https://packaging.python.org/en/latest/specifications/entry-points/
.. _Python's import system: https://docs.python.org/3/reference/import.html
+4 -74
View File
@@ -20,7 +20,7 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = "responder" project = "responder"
copyright = "2018-2026, A Kenneth Reitz project" copyright = "2018, A Kenneth Reitz project"
author = "Kenneth Reitz" author = "Kenneth Reitz"
# The short X.Y version # The short X.Y version
@@ -57,11 +57,6 @@ extensions = [
"sphinx.ext.ifconfig", "sphinx.ext.ifconfig",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx.ext.githubpages", "sphinx.ext.githubpages",
"myst_parser",
"sphinx_copybutton",
"sphinx_design",
"sphinx_design_elements",
"sphinxext.opengraph",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@@ -71,7 +66,7 @@ templates_path = ["_templates"]
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = {".rst": "restructuredtext"} source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = "index"
@@ -81,7 +76,7 @@ master_doc = "index"
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = "en" language = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@@ -216,77 +211,12 @@ epub_exclude_files = ["search.html"]
# -- Extension configuration ------------------------------------------------- # -- Extension configuration -------------------------------------------------
# -- Options for link checker ----------------------------------------------
linkcheck_ignore = [
# Feldroy.com links are ignored because it blocks GHA.
r"https://www.feldroy.com/.*",
]
linkcheck_anchors_ignore_for_url = [
# Requires JavaScript.
# After opting-in to new GitHub issues, Sphinx can no longer grok the HTML anchor references.
r"https://github.com",
]
# -- Options for intersphinx extension --------------------------------------- # -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} intersphinx_mapping = {"https://docs.python.org/": None}
# -- Options for todo extension ---------------------------------------------- # -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True todo_include_todos = True
# -- Options for MyST --------------------------------------------------------
myst_heading_anchors = 3
myst_enable_extensions = [
"attrs_block",
"attrs_inline",
"colon_fence",
"deflist",
"fieldlist",
"html_admonition",
"html_image",
"linkify",
"replacements",
"strikethrough",
"substitution",
"tasklist",
]
myst_substitutions = {}
# -- Options for OpenGraph ---------------------------------------------------
#
# When making changes, check them using the RTD PR preview URL on https://www.opengraph.xyz/.
#
# About text lengths
#
# Original documentation says:
# - ogp_description_length
# Configure the amount of characters taken from a page. The default of 200 is probably good
# for most people. If something other than a number is used, it defaults back to 200.
# -- https://sphinxext-opengraph.readthedocs.io/en/latest/#options
#
# Other people say:
# - og:title 40 chars
# - og:description has 2 max lengths:
# When the link is used in a Post, it's 300 chars. When a link is used in a Comment, it's 110 chars.
# So you can either treat it as 110, or, write your Descriptions to 300 but make sure the first 110
# is the critical part and still makes sense when it gets cut off.
# -- https://stackoverflow.com/questions/8914476/facebook-open-graph-meta-tags-maximum-content-length
ogp_site_url = "https://responder.kennethreitz.org/"
ogp_description_length = 300
ogp_site_name = "Responder Documentation"
ogp_image = "https://responder.kennethreitz.org/_static/responder.png"
ogp_image_alt = False
ogp_use_first_image = False
ogp_type = "website"
ogp_enable_meta_description = True
# -- Options for sphinx-copybutton -------------------------------------------
copybutton_remove_prompts = True
copybutton_line_continuation_character = "\\"
copybutton_prompt_text = r">>> |\.\.\. |\$ |sh\$ |PS> |cr> |mysql> |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
copybutton_prompt_is_regexp = True
+21 -18
View File
@@ -6,30 +6,33 @@ You can deploy Responder anywhere you can deploy a basic Python application.
Docker Deployment Docker Deployment
----------------- -----------------
Assuming an existing ``api.py`` containing your Responder application. Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``:: ``Dockerfile``::
FROM python:3.13-slim FROM kennethreitz/pipenv
WORKDIR /app ENV PORT '80'
COPY . . COPY . /app
RUN pip install responder CMD python3 api.py
ENV PORT=80
EXPOSE 80 EXPOSE 80
CMD ["python", "api.py"]
That's it! That's it!
Cloud Deployment Heroku Deployment
---------------- -----------------
Responder automatically honors the ``PORT`` environment variable, which is
set by most cloud platforms (Fly.io, Railway, Render, Google Cloud Run, etc.).
The basics:: The basics::
$ mkdir my-api $ mkdir my-api
$ cd my-api $ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out an ``api.py``:: Write out an ``api.py``::
@@ -44,12 +47,12 @@ Write out an ``api.py``::
if __name__ == "__main__": if __name__ == "__main__":
api.run() api.run()
Deploy with your platform of choice. Responder will bind to ``0.0.0.0`` Write out a ``Procfile``::
on the port specified by ``PORT`` automatically.
Running with Uvicorn Directly web: python api.py
-----------------------------
For production deployments, you can also run your app directly with uvicorn:: That's it! Next, we commit and push to Heroku::
uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4 $ git add -A
$ git commit -m 'initial commit'
$ git push heroku master
+33 -66
View File
@@ -6,23 +6,19 @@
A familiar HTTP Service Framework A familiar HTTP Service Framework
================================= =================================
|ci-tests| |version| |license| |python-versions| |downloads| |contributors| |say-thanks| |Build Status| |image1| |image2| |image3| |image4| |image5|
.. |ci-tests| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg .. |Build Status| image:: https://travis-ci.org/kennethreitz/responder.svg?branch=master
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml :target: https://travis-ci.org/kennethreitz/responder
.. |ci-docs| image:: https://github.com/kennethreitz/responder/actions/workflows/docs.yaml/badge.svg .. |image1| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/docs.yaml
.. |version| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/ :target: https://pypi.org/project/responder/
.. |license| image:: https://img.shields.io/pypi/l/responder.svg .. |image2| image:: https://img.shields.io/pypi/l/responder.svg
:target: https://pypi.org/project/responder/ :target: https://pypi.org/project/responder/
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/responder.svg .. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
:target: https://pypi.org/project/responder/ :target: https://pypi.org/project/responder/
.. |downloads| image:: https://static.pepy.tech/badge/responder/month .. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://www.pepy.tech/projects/responder
.. |contributors| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
:target: https://github.com/kennethreitz/responder/graphs/contributors :target: https://github.com/kennethreitz/responder/graphs/contributors
.. |say-thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg .. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz :target: https://saythanks.io/to/kennethreitz
.. code:: python .. code:: python
@@ -38,24 +34,23 @@ A familiar HTTP Service Framework
if __name__ == '__main__': if __name__ == '__main__':
api.run() api.run()
Responder is powered by `Starlette`_. Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
The example program demonstrates an `ASGI`_ application using `Responder`_, This gets you a ASGI app, with a production static files server
including production-ready components like the `uvicorn`_ webserver, based (`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
on `uvloop`_, and the `Jinja`_ templating library pre-installed. pre-installed, jinja2 templating (without additional imports), and a
The ``async`` declaration within the example program is optional. production webserver based on uvloop, serving up requests with
automatic gzip compression.
Features Features
-------- --------
- A pleasant API, with a single import statement. - A pleasant API, with a single import statement.
- Class-based views without inheritance. - Class-based views without inheritance.
- `ASGI`_, the future of Python web services. - `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
- Asynchronous Python frameworks and applications.
- Automatic gzip compression.
- WebSocket support! - WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute. - The ability to mount any ASGI / WSGI app at a subroute.
- `f-string syntax`_ route declaration. - `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. - Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``. - Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support! - GraphQL (with *GraphiQL*) support!
@@ -66,23 +61,30 @@ Testimonials
------------ ------------
“Pleasantly very taken with python-responder. “Pleasantly very taken with python-responder.
`@kennethreitz`_ at his absolute best.” `@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
best.”
—Rudraksh M.K.
— Rudraksh M.K.
.. ..
"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." "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`_ Tom Christie, author of `Django REST Framework`_
.. ..
“I love that you are exploring new patterns. Go go go!” “I love that you are exploring new patterns. Go go go!”
— Danny Greenfield, author of `Two Scoops of Django`_ — 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 User Guides
----------- -----------
@@ -94,38 +96,17 @@ User Guides
deployment deployment
testing testing
api api
cli
Installing Responder Installing Responder
-------------------- --------------------
Use ``uv`` for fast installation.
.. code-block:: shell .. code-block:: shell
uv pip install --upgrade 'responder' $ pipenv install responder
✨🍰✨
Or use standard pip where ``uv`` is not available. Only **Python 3.6+** is supported.
.. code-block:: shell
pip install --upgrade 'responder'
Responder supports **Python 3.9+**.
Development
-----------
If you are looking at installing Responder
for hacking on it, please refer to the :ref:`sandbox` documentation.
.. toctree::
:maxdepth: 1
changes
Sandbox <sandbox>
backlog
The Basic Idea The Basic Idea
@@ -142,14 +123,14 @@ The primary concept here is to bring the niceties that are brought forth from bo
Ideas Ideas
----- -----
- Flask-style route expression, with new capabilities -- using Python's f-string syntax. - 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. - 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** powered by Starlette's TestClient. - **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily. - The ability to mount other WSGI apps easily.
- Automatic gzipped-responses. - 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. - 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. - A production static files server is built-in.
- `uvicorn`_ 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`_ attacks, making Nginx unnecessary in production. - `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. - GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
@@ -159,17 +140,3 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`
.. _@kennethreitz: https://x.com/kennethreitz42
.. _ASGI: https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _f-string syntax: https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals
.. _Jinja: https://jinja.palletsprojects.com/en/stable/
.. _ServeStatic: https://archmonger.github.io/ServeStatic/latest/
.. _Slowloris: https://en.wikipedia.org/wiki/Slowloris_(computer_security)
.. _Starlette: https://www.starlette.io/
.. _Responder: https://responder.kennethreitz.org/
.. _Two Scoops of Django: https://www.feldroy.com/two-scoops-press#two-scoops-of-django
.. _uvicorn: https://www.uvicorn.org/
.. _uvloop: https://uvloop.readthedocs.io/
+10 -9
View File
@@ -54,7 +54,7 @@ Type convertors are also available::
async def add(req, resp, *, a, b): async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}" resp.text = f"{a} + {b} = {a + b}"
Supported types: ``str``, ``int``, ``float``, ``uuid``, and ``path``. Supported types: ``str``, ``int`` and ``float``.
Returning JSON / YAML Returning JSON / YAML
--------------------- ---------------------
@@ -73,7 +73,7 @@ If the client requests YAML instead (with a header of ``Accept: application/x-ya
Rendering a Template Rendering a Template
-------------------- --------------------
Responder provides a built-in light `Jinja`_ wrapper ``templates.Templates`` Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
Usage:: Usage::
@@ -158,19 +158,20 @@ Here's a sample code to post a file with background::
@api.background.task @api.background.task
def process_data(data): def process_data(data):
with open(f"./{data['file']['filename']}", 'wb') as f: f = open('./{}'.format(data['file']['filename']), 'w')
f.write(data['file']['content']) f.write(data['file']['content'].decode('utf-8'))
f.close()
data = await req.media(format='files') data = await req.media(format='files')
process_data(data) process_data(data)
resp.media = {'success': 'ok'} resp.media = {'success': 'ok'}
You can test file uploads using the built-in test client:: You can send a file easily with requests::
files = {'file': ('hello.txt', b'hello, world!', 'text/plain')} import requests
r = api.requests.post(api.url_for(upload_file), files=files)
print(r.json())
data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
r = requests.post('http://127.0.0.1:8210/file', files=data)
.. _Jinja: https://jinja.palletsprojects.com/en/stable/ print(r.text)
-36
View File
@@ -1,36 +0,0 @@
(sandbox)=
# Development Sandbox
## Setup
Set up a development sandbox.
Acquire sources and create virtualenv.
```shell
git clone https://github.com/kennethreitz/responder.git
cd responder
uv venv
```
Install project in editable mode, including
all development tools.
```shell
uv pip install --upgrade --editable '.[develop,docs,release,test]'
```
## Operations
Run tests.
```shell
source .venv/bin/activate
pytest
```
Format code.
```shell
ruff format .
ruff check --fix .
```
Documentation authoring.
```shell
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
```
+37 -4
View File
@@ -1,16 +1,16 @@
Building and Testing with Responder Building and Testing with Responder
=================================== ===================================
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient). 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 and testing a Responder application. Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
The Basics The Basics
---------- ----------
Your project should look like this:: Your repository should look like this::
api.py test_api.py Pipfile Pipfile.lock api.py test_api.py
``$ cat api.py``:: ``$ cat api.py``::
@@ -25,6 +25,26 @@ Your project should look like this::
if __name__ == "__main__": if __name__ == "__main__":
api.run() api.run()
``$ cat Pipfile``::
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Writing Tests Writing Tests
------------- -------------
@@ -46,3 +66,16 @@ Writing Tests
... ...
========================== 1 passed in 0.10 seconds ========================== ========================== 1 passed in 0.10 seconds ==========================
(Optional) Proper Python Package
--------------------------------
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
1. A `proper setup.py <https://github.com/kennethreitz/setup.py>`_ file.
2. ``$ pipenv install -e . --dev``
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``.
This will ensure that your application gets installed in every developer's environment, using Pipenv.
+11 -91
View File
@@ -2,21 +2,6 @@ Feature Tour
============ ============
Route Method Filtering
----------------------
You can restrict routes to specific HTTP methods::
@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}
Class-Based Views Class-Based Views
----------------- -----------------
@@ -30,61 +15,6 @@ Class-based views (and setting some headers and stuff)::
resp.status_code = api.status_codes.HTTP_416 resp.status_code = api.status_codes.HTTP_416
Lifespan Events
---------------
Use the lifespan context manager for startup and shutdown logic::
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
# Startup: connect to database, etc.
print("Starting up...")
yield
# Shutdown: clean up resources
print("Shutting down...")
api = responder.API(lifespan=lifespan)
You can also use the traditional event decorators::
@api.on_event('startup')
async def startup():
print("Starting up...")
@api.on_event('shutdown')
async def shutdown():
print("Shutting down...")
Serving Files
-------------
Serve files from disk with automatic content-type detection::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can also specify the content type explicitly::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
Custom Error Handling
---------------------
Register handlers for specific exception types::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Background Tasks Background Tasks
---------------- ----------------
@@ -104,15 +34,10 @@ Here, you can spawn off a background thread to run any function, out-of-request:
GraphQL GraphQL
------- -------
Responder supports GraphQL, a query language for APIs that enables clients to
request exactly the data they need.
For more information about GraphQL, visit https://graphql.org/.
Serve a GraphQL API:: Serve a GraphQL API::
import graphene import graphene
from responder.ext.graphql import GraphQLView
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger")) hello = graphene.String(name=graphene.String(default_value="stranger"))
@@ -121,7 +46,7 @@ Serve a GraphQL API::
return f"Hello {name}" return f"Hello {name}"
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
view = GraphQLView(api=api, schema=schema) view = responder.ext.GraphQLView(api=api, schema=schema)
api.add_route("/graph", view) api.add_route("/graph", view)
@@ -133,18 +58,12 @@ You can make use of Responder's Request and Response objects in your GraphQL res
OpenAPI Schema Support OpenAPI Schema Support
---------------------- ----------------------
Responder comes with built-in support for OpenAPI / marshmallow. Responder comes with built-in support for OpenAPI / marshmallow
.. note:: New in Responder `1.4.0`::
If you're upgrading from a previous version, note that the OpenAPI module
has been renamed from ``responder.ext.schema`` to ``responder.ext.openapi``.
Update your imports accordingly.
New in Responder 1.4.0::
import responder import responder
from responder.ext.openapi import OpenAPISchema from responder.ext.schema import Schema as OpenAPISchema
from marshmallow import Schema, fields from marshmallow import Schema, fields
contact = { contact = {
@@ -275,11 +194,12 @@ Responder can automatically supply API Documentation for you. Using the example
The new and recommended way:: The new and recommended way::
from responder.ext.openapi import OpenAPISchema ...
from responder.ext.schema import Schema
...
api = responder.API() api = responder.API()
schema = OpenAPISchema( schema = Schema(
app=api, app=api,
title="Web Service", title="Web Service",
version="1.0", version="1.0",
@@ -294,7 +214,7 @@ The new and recommended way::
) )
The old way:: The old way ::
api = responder.API( api = responder.API(
title="Web Service", title="Web Service",
@@ -363,7 +283,7 @@ Supported directives:
* ``secure`` - Defaults to ``False``. * ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``. * ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_ For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives>`_
Using Cookie-Based Sessions Using Cookie-Based Sessions
@@ -433,7 +353,7 @@ Closing the connection::
Using Requests Test Client Using Requests Test Client
-------------------------- --------------------------
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient). 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):: Here's an example of a test (written with pytest)::
-19
View File
@@ -1,19 +0,0 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "hello, world!"
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
-26
View File
@@ -1,26 +0,0 @@
# Example showing the lifespan context manager pattern.
# https://pypi.org/project/responder/
from contextlib import asynccontextmanager
import responder
@asynccontextmanager
async def lifespan(app):
# Startup: initialize resources
print("Starting up...")
yield
# Shutdown: clean up resources
print("Shutting down...")
api = responder.API(lifespan=lifespan)
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
-25
View File
@@ -1,25 +0,0 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder
api = responder.API()
@api.route("/")
async def index(req, resp):
resp.text = "Welcome"
@api.route("/user")
async def user_create(req, resp):
data = await req.media()
resp.text = f"Hello, {data['username']}"
@api.route("/user/{identifier}")
async def user_get(req, resp, *, identifier):
resp.text = f"Hello, user {identifier}"
if __name__ == "__main__":
api.run()
-183
View File
@@ -1,183 +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",
"pueblo[sfa-full]>=0.0.11",
"python-multipart",
"starlette[full]>=0.40",
"uvicorn[standard]",
]
[project.optional-dependencies]
develop = [
"pyproject-fmt",
"ruff",
"validate-pyproject",
]
docs = [
"alabaster<1.1",
"myst-parser[linkify]",
"sphinx>=5,<9",
"sphinx-autobuild",
"sphinx-copybutton",
"sphinx-design-elements",
"sphinxext.opengraph",
]
release = ["build", "twine"]
test = [
"flask",
"mypy",
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-rerunfailures",
]
[project.scripts]
responder = "responder.ext.cli:cli"
[project.urls]
Homepage = "https://github.com/kennethreitz/responder"
Documentation = "https://responder.kennethreitz.org"
Repository = "https://github.com/kennethreitz/responder"
Issues = "https://github.com/kennethreitz/responder/issues"
[tool.setuptools.dynamic]
version = {attr = "responder.__version__.__version__"}
[tool.setuptools.package-data]
responder = ["py.typed"]
[tool.setuptools.packages.find]
exclude = ["tests"]
[tool.ruff]
line-length = 90
extend-exclude = [
"docs/source/conf.py",
]
lint.select = [
# Builtins
"A",
# Bugbear
"B",
# comprehensions
"C4",
# Pycodestyle
"E",
# eradicate
"ERA",
# Pyflakes
"F",
# isort
"I",
# pandas-vet
"PD",
# return
"RET",
# Bandit
"S",
# print
"T20",
"W",
# flake8-2020
"YTT",
]
lint.extend-ignore = [
"S101", # Allow use of `assert`.
]
lint.per-file-ignores."responder/util/cmd.py" = [ "A005" ] # Module shadows a Python standard-library module
lint.per-file-ignores."tests/*" = [
"ERA001", # Found commented-out code.
"S101", # Allow use of `assert`, and `print`.
]
[tool.pytest.ini_options]
addopts = """
-rfEXs -p pytester --strict-markers --verbosity=3
--cov --cov-report=term-missing --cov-report=xml
"""
filterwarnings = [
"error::UserWarning",
]
log_level = "DEBUG"
log_cli_level = "DEBUG"
log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
minversion = "2.0"
testpaths = [
"responder",
"tests",
]
markers = [
]
xfail_strict = true
[tool.coverage.run]
branch = false
omit = [
"*.html",
"tests/*",
]
[tool.coverage.report]
fail_under = 0
show_missing = true
exclude_lines = [
"# pragma: no cover",
"raise NotImplemented",
]
[tool.mypy]
packages = [
"responder",
]
exclude = [
]
check_untyped_defs = true
explicit_package_bases = true
ignore_missing_imports = true
implicit_optional = true
install_types = true
namespace_packages = true
non_interactive = true
+4
View File
@@ -0,0 +1,4 @@
[pytest]
;addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
+5
View File
@@ -0,0 +1,5 @@
build:
image: latest
python:
version: 3.6
+1 -17
View File
@@ -1,18 +1,2 @@
""" from .core import *
Responder - a familiar HTTP Service Framework.
This module exports the core functionality of the Responder framework,
including the API, Request, and Response classes.
"""
from . import ext from . import ext
from .__version__ import __version__
from .core import API, Request, Response
__all__ = [
"API",
"Request",
"Response",
"__version__",
"ext",
]
+4
View File
@@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.1.0" __version__ = "2.0.4"
+74 -160
View File
@@ -1,37 +1,41 @@
import asyncio import json
import os import os
from pathlib import Path from pathlib import Path
__all__ = ["API"] import jinja2
import uvicorn import uvicorn
from starlette.middleware.cors import CORSMiddleware from starlette.exceptions import ExceptionMiddleware
from starlette.middleware.wsgi import WSGIMiddleware
from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.gzip import GZipMiddleware from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Lifespan
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
from . import status_codes from . import models, status_codes
from .background import BackgroundQueue from .background import BackgroundQueue
from .formats import get_formats from .formats import get_formats
from .models import Request, Response
from .routes import Router from .routes import Router
from .statics import DEFAULT_API_THEME, DEFAULT_CORS_PARAMS, DEFAULT_SECRET_KEY
from .ext.schema import Schema as OpenAPISchema
from .staticfiles import StaticFiles from .staticfiles import StaticFiles
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
from .templates import Templates from .templates import Templates
class API: class API:
"""The primary web-service class. """The primary web-service class.
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist. :param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist. :param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped. :param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs. :param enable_hsts: If ``True``, send all responses to HTTPS URLs.
:param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui`` """
""" # noqa: E501
status_codes = status_codes status_codes = status_codes
@@ -40,12 +44,13 @@ class API:
*, *,
debug=False, debug=False,
title=None, title=None,
version=None, version="1.0",
description=None, description=None,
terms_of_service=None, terms_of_service=None,
contact=None, contact=None,
license=None, # noqa: A002 license=None,
openapi=None, openapi=None,
openapi_version="3.0.2",
openapi_route="/schema.yml", openapi_route="/schema.yml",
static_dir="static", static_dir="static",
static_route="/static", static_route="/static",
@@ -57,19 +62,17 @@ class API:
cors=False, cors=False,
cors_params=DEFAULT_CORS_PARAMS, cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None, allowed_hosts=None,
openapi_theme=DEFAULT_OPENAPI_THEME,
lifespan=None,
): ):
self.background = BackgroundQueue() self.background = BackgroundQueue()
self.secret_key = secret_key self.secret_key = secret_key
self.router = Router(lifespan=lifespan) self.router = Router()
if static_dir is not None: if static_dir is not None:
if static_route is None: if static_route is None:
static_route = "" static_route = static_dir
static_dir = Path(static_dir).resolve() static_dir = Path(os.path.abspath(static_dir))
self.static_dir = static_dir self.static_dir = static_dir
self.static_route = static_route self.static_route = static_route
@@ -80,15 +83,22 @@ class API:
self.debug = debug self.debug = debug
if not allowed_hosts: if not allowed_hosts:
# if not debug:
# raise RuntimeError(
# "You need to specify `allowed_hosts` when debug is set to False"
# )
allowed_hosts = ["*"] allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts self.allowed_hosts = allowed_hosts
if self.static_dir is not None: if self.static_dir is not None:
self.static_dir.mkdir(parents=True, exist_ok=True) os.makedirs(self.static_dir, exist_ok=True)
if self.static_dir is not None:
self.mount(self.static_route, self.static_app) self.mount(self.static_route, self.static_app)
self.formats = get_formats() self.formats = get_formats()
# Cached requests session.
self._session = None self._session = None
self.default_endpoint = None self.default_endpoint = None
@@ -106,19 +116,11 @@ class API:
self.add_middleware(SessionMiddleware, secret_key=self.secret_key) self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
if openapi or docs_route: if openapi or docs_route:
try:
from .ext.openapi import OpenAPISchema
except ImportError as ex:
raise ImportError(
"The dependencies for the OpenAPI extension are not installed. "
"Install them using: pip install responder"
) from ex
self.openapi = OpenAPISchema( self.openapi = OpenAPISchema(
app=self, app=self,
title=title, title=title,
version=version, version=version,
openapi=openapi, openapi=openapi_version,
docs_route=docs_route, docs_route=docs_route,
description=description, description=description,
terms_of_service=terms_of_service, terms_of_service=terms_of_service,
@@ -126,15 +128,13 @@ class API:
license=license, license=license,
openapi_route=openapi_route, openapi_route=openapi_route,
static_route=static_route, static_route=static_route,
openapi_theme=openapi_theme,
) )
# TODO: Update docs for templates
self.templates = Templates(directory=templates_dir) self.templates = Templates(directory=templates_dir)
self.requests = (
@property self.session()
def requests(self): ) #: A Requests session that is connected to the ASGI app.
"""A test client connected to the ASGI app. Lazily initialized."""
return self.session()
@property @property
def static_app(self): def static_app(self):
@@ -153,59 +153,9 @@ class API:
def add_middleware(self, middleware_cls, **middleware_config): def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config) self.app = middleware_cls(self.app, **middleware_config)
def exception_handler(self, exception_cls):
"""Register a handler for a specific exception type.
Usage::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
"""
def decorator(func):
async def _handler(request, exc):
from starlette.responses import Response as StarletteResp
req = Request(request.scope, request.receive, formats=get_formats())
resp = Response(req=req, formats=get_formats())
if asyncio.iscoroutinefunction(func):
await func(req, resp, exc)
else:
func(req, resp, exc)
if resp.status_code is None:
resp.status_code = 500
body, headers = await resp.body
return StarletteResp(
content=body, status_code=resp.status_code, headers=headers
)
# Register with the ExceptionMiddleware
self.router._exception_handlers = getattr(
self.router, "_exception_handlers", {}
)
self.router._exception_handlers[exception_cls] = _handler
# Also register on the ASGI app chain
from starlette.middleware.exceptions import ExceptionMiddleware as EM
app = self.app
while app is not None:
if isinstance(app, EM):
app.add_exception_handler(exception_cls, _handler)
break
app = getattr(app, "app", None)
return func
return decorator
def schema(self, name, **options): def schema(self, name, **options):
""" """Decorator for creating new routes around function and class definitions.
Decorator for creating new routes around function and class definitions.
Usage:: Usage::
from marshmallow import Schema, fields from marshmallow import Schema, fields
@api.schema("Pet") @api.schema("Pet")
class PetSchema(Schema): class PetSchema(Schema):
@@ -222,12 +172,11 @@ class API:
"""Given a path portion of a URL, tests that it matches against any registered route. """Given a path portion of a URL, tests that it matches against any registered route.
:param path: The path portion of a URL, to test all known routes against. :param path: The path portion of a URL, to test all known routes against.
""" # noqa: E501 (Line too long) """
for route in self.router.routes: for route in self.router.routes:
match, _ = route.matches(path) match, _ = route.matches(path)
if match: if match:
return route return route
return None
def add_route( def add_route(
self, self,
@@ -239,18 +188,16 @@ class API:
check_existing=True, check_existing=True,
websocket=False, websocket=False,
before_request=False, before_request=False,
methods=None,
): ):
"""Adds a route to the API. """Adds a route to the API.
:param route: A string representation of the route. :param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, or a class. :param endpoint: The endpoint for the route -- can be a callable, or a class.
:param default: If ``True``, all unknown requests will route to this view. :param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html". :param static: If ``True``, and no endpoint was passed, render "static/index.html", and it will become a default route.
Also, it will become a default route. """
:param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).
""" # noqa: E501
# Path
if static: if static:
assert self.static_dir is not None assert self.static_dir is not None
if not endpoint: if not endpoint:
@@ -264,35 +211,27 @@ class API:
websocket=websocket, websocket=websocket,
before_request=before_request, before_request=before_request,
check_existing=check_existing, check_existing=check_existing,
methods=methods,
) )
async def _static_response(self, req, resp): async def _static_response(self, req, resp):
assert self.static_dir is not None assert self.static_dir is not None
index = (self.static_dir / "index.html").resolve() index = (self.static_dir / "index.html").resolve()
if index.exists(): if os.path.exists(index):
resp.html = index.read_text() with open(index, "r") as f:
resp.html = f.read()
else: else:
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined] resp.status_code = status_codes.HTTP_404
resp.text = "Not found." resp.text = "Not found."
def redirect( def redirect(
self, self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
resp,
location,
*,
set_text=True,
status_code=status_codes.HTTP_301, # type: ignore[attr-defined]
): ):
""" """Redirects a given response to a given location.
Redirects a given response to a given location.
:param resp: The Response to mutate. :param resp: The Response to mutate.
:param location: The location of the redirect. :param location: The location of the redirect.
:param set_text: If ``True``, sets the Redirect body content automatically. :param set_text: If ``True``, sets the Redirect body content automatically.
:param status_code: an `API.status_codes` attribute, or an integer, :param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
representing the HTTP status code of the redirect.
""" """
resp.redirect(location, set_text=set_text, status_code=status_code) resp.redirect(location, set_text=set_text, status_code=status_code)
@@ -325,7 +264,7 @@ class API:
:param handler: The function to run. Can be either a function or a coroutine. :param handler: The function to run. Can be either a function or a coroutine.
""" """
self.router.add_event_handler(event_type, handler) self.router.lifespan_handler.add_event_handler(event_type, handler)
def route(self, route=None, **options): def route(self, route=None, **options):
"""Decorator for creating new routes around function and class definitions. """Decorator for creating new routes around function and class definitions.
@@ -344,50 +283,26 @@ class API:
return decorator return decorator
def graphql(self, route="/graphql", *, schema):
"""Mount a GraphQL API at the given route.
Usage::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
:param route: The URL path for the GraphQL endpoint.
:param schema: A Graphene schema instance.
"""
from .ext.graphql import GraphQLView
self.add_route(route, GraphQLView(api=self, schema=schema))
def mount(self, route, app): def mount(self, route, app):
"""Mounts an WSGI / ASGI application at a given route. """Mounts an WSGI / ASGI application at a given route.
:param route: String representation of the route to be used :param route: String representation of the route to be used (shouldn't be parameterized).
(shouldn't be parameterized).
:param app: The other WSGI / ASGI app. :param app: The other WSGI / ASGI app.
""" """
self.router.apps.update({route: app}) self.router.apps.update({route: app})
def session(self, base_url="http://;"): def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a Starlette TestClient instance, """Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the Responder application.
able to send HTTP requests to the Responder application.
:param base_url: The base URL for the test client. :param base_url: The URL to mount the connection adaptor to.
""" """
if self._session is None: if self._session is None:
from starlette.testclient import TestClient
self._session = TestClient(self, base_url=base_url) self._session = TestClient(self, base_url=base_url)
return self._session return self._session
def url_for(self, endpoint, **params): def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route. """Given an endpoint, returns a rendered URL for its route.
:param endpoint: The route endpoint you're searching for. :param endpoint: The route endpoint you're searching for.
@@ -396,49 +311,48 @@ class API:
return self.router.url_for(endpoint, **params) return self.router.url_for(endpoint, **params)
def template(self, filename, *args, **kwargs): def template(self, filename, *args, **kwargs):
r"""Render a Jinja2 template file with the provided values. """Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param filename: The filename of the jinja2 template, in ``templates_dir``. :param filename: The filename of the jinja2 template, in ``templates_dir``.
:param \*args: Data to pass into the template. :param *args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template. :param *kwargs: Date to pass into the template.
""" """
return self.templates.render(filename, *args, **kwargs) return self.templates.render(filename, *args, **kwargs)
def template_string(self, source, *args, **kwargs): def template_string(self, source, *args, **kwargs):
r"""Render a Jinja2 template string with the provided values. """Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param source: The template to use, a Jinja2 template string. :param source: The template to use.
:param \*args: Data to pass into the template. :param *args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template. :param **kwargs: Data to pass into the template.
""" """
return self.templates.render_string(source, *args, **kwargs) return self.templates.render_string(source, *args, **kwargs)
def serve(self, *, address=None, port=None, debug=False, **options): def serve(self, *, address=None, port=None, debug=False, **options):
""" """Runs the application with uvicorn. If the ``PORT`` environment
Run the application with uvicorn. variable is set, requests will be served on that port automatically to all
known hosts.
If the ``PORT`` environment variable is set, requests will be served on that port
automatically to all known hosts.
:param address: The address to bind to. :param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random. :param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: Whether to run application in debug mode. :param debug: Run uvicorn server in debug mode.
:param options: Additional keyword arguments to send to ``uvicorn.run()``. :param options: Additional keyword arguments to send to ``uvicorn.run()``.
""" # noqa: E501 """
if "PORT" in os.environ: if "PORT" in os.environ:
if address is None: if address is None:
address = "0.0.0.0" # noqa: S104 address = "0.0.0.0"
port = int(os.environ["PORT"]) port = int(os.environ["PORT"])
if address is None: if address is None:
address = "127.0.0.1" address = "127.0.0.1"
if port is None: if port is None:
port = 5042 port = 5042
if debug:
options["log_level"] = "debug"
uvicorn.run(self, host=address, port=port, **options) def spawn():
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
def run(self, **kwargs): def run(self, **kwargs):
if "debug" not in kwargs: if "debug" not in kwargs:
+8 -6
View File
@@ -1,12 +1,10 @@
import asyncio import asyncio
import functools
import concurrent.futures import concurrent.futures
import multiprocessing import multiprocessing
import traceback import traceback
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
__all__ = ["BackgroundQueue"]
class BackgroundQueue: class BackgroundQueue:
def __init__(self, n=None): def __init__(self, n=None):
@@ -18,6 +16,9 @@ class BackgroundQueue:
self.results = [] self.results = []
def run(self, f, *args, **kwargs): def run(self, f, *args, **kwargs):
self.pool._max_workers = self.n
self.pool._adjust_thread_count()
f = self.pool.submit(f, *args, **kwargs) f = self.pool.submit(f, *args, **kwargs)
self.results.append(f) self.results.append(f)
return f return f
@@ -26,7 +27,7 @@ class BackgroundQueue:
def on_future_done(fs): def on_future_done(fs):
try: try:
fs.result() fs.result()
except Exception: except:
traceback.print_exc() traceback.print_exc()
def do_task(*args, **kwargs): def do_task(*args, **kwargs):
@@ -38,5 +39,6 @@ class BackgroundQueue:
async def __call__(self, func, *args, **kwargs) -> None: async def __call__(self, func, *args, **kwargs) -> None:
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
return await asyncio.create_task(func(*args, **kwargs)) return await asyncio.ensure_future(func(*args, **kwargs))
return await run_in_threadpool(func, *args, **kwargs) else:
return await run_in_threadpool(func, *args, **kwargs)
+43
View File
@@ -0,0 +1,43 @@
"""Responder.
Usage:
responder
responder run [--build] [--debug] <module>
responder build
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
"""
import os
import docopt
from .__version__ import __version__
def cli():
args = docopt.docopt(
__doc__, argv=None, help=True, version=__version__, options_first=False
)
module = args["<module>"]
build = args["build"] or args["--build"]
run = args["run"]
if build:
os.system("npm run build")
if run:
split_module = module.split(":")
if len(split_module) > 1:
module = split_module[0]
prop = split_module[1]
else:
prop = "api"
app = __import__(module)
getattr(app, prop).run()
+1 -6
View File
@@ -1,8 +1,3 @@
from .api import API from .api import API
from .models import Request, Response from .models import Request, Response
from .cli import cli
__all__ = [
"API",
"Request",
"Response",
]
+1
View File
@@ -0,0 +1 @@
from .graphql import GraphQLView
-129
View File
@@ -1,129 +0,0 @@
"""
Responder CLI.
A web framework for Python.
Commands:
run Start the application server
build Build frontend assets using npm
Usage:
responder
responder run [--debug] [--limit-max-requests=] <target>
responder build [<target>]
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
--debug Enable debug mode with verbose logging.
--limit-max-requests=<n> Maximum number of requests to handle before shutting down.
Arguments:
<target> For run: Python module specifier (e.g., "app:api" loads api from app.py)
Format: "module.submodule:variable_name" where variable_name is your API instance
For build: Directory containing package.json (default: current directory)
Examples:
responder run app:api # Run the 'api' instance from app.py
responder run myapp/core.py:application # Run the 'application' instance from myapp/core.py
responder build # Build frontend assets
""" # noqa: E501
import logging
import platform
import subprocess
import sys
import typing as t
from pathlib import Path
import docopt
from responder.__version__ import __version__
from responder.util.python import InvalidTarget, load_target
logger = logging.getLogger(__name__)
def cli() -> None:
"""
Main entry point for the Responder CLI.
Parses command line arguments and executes the appropriate command.
Supports running the application, building assets, and displaying version info.
"""
args = docopt.docopt(__doc__, argv=None, version=__version__, options_first=False)
setup_logging(args["--debug"])
target: t.Optional[str] = args["<target>"]
build: bool = args["build"]
debug: bool = args["--debug"]
run: bool = args["run"]
if build:
target_path = Path(target).resolve() if target else Path.cwd()
if not target_path.is_dir() or not (target_path / "package.json").exists():
logger.error(
f"Invalid target directory or missing package.json: {target_path}"
)
sys.exit(1)
npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"
try:
logger.info("Starting frontend asset build")
# S603, S607 are addressed by validating the target directory.
subprocess.check_call( # noqa: S603, S607
[npm_cmd, "run", "build"],
cwd=target_path,
timeout=300,
)
logger.info("Frontend asset build completed successfully")
except FileNotFoundError:
logger.error("npm not found. Please install Node.js and npm.")
sys.exit(1)
except subprocess.CalledProcessError as e:
logger.error(f"Build failed with exit code {e.returncode}")
sys.exit(1)
if run:
if not target:
logger.error("Target argument is required for run command")
sys.exit(1)
# Maximum request limit. Terminating afterward. Suitable for software testing.
limit_max_requests = args["--limit-max-requests"]
if limit_max_requests is not None:
try:
limit_max_requests = int(limit_max_requests)
if limit_max_requests <= 0:
logger.error("limit-max-requests must be a positive integer")
sys.exit(1)
except ValueError:
logger.error("limit-max-requests must be a valid integer")
sys.exit(1)
# Load application from target.
try:
api = load_target(target=target)
except InvalidTarget as ex:
raise ValueError(
f"{ex}. "
"Use either a Python module entrypoint specification, "
"a filesystem path, or a remote URL. "
"See also https://responder.kennethreitz.org/cli.html."
) from ex
# Launch Responder API server (uvicorn).
api.run(debug=debug, limit_max_requests=limit_max_requests)
def setup_logging(debug: bool) -> None:
"""
Configure logging based on debug mode.
Args:
debug: When True, sets logging level to DEBUG; otherwise, sets to INFO
"""
log_level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
+28 -24
View File
@@ -1,4 +1,7 @@
import json import json
from functools import partial
from graphql_server import default_format_error, encode_execution_results, json_encode
from .templates import GRAPHIQL from .templates import GRAPHIQL
@@ -9,19 +12,24 @@ class GraphQLView:
self.schema = schema self.schema = schema
@staticmethod @staticmethod
async def _resolve_graphql_query(req, resp): async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
if "json" in req.mimetype: if "json" in req.mimetype:
json_media = await req.media("json") json_media = await req.media("json")
if "query" not in json_media:
resp.status_code = 400
resp.media = {"errors": ["'query' key is required in the JSON payload"]}
return None, None, None
return ( return (
json_media["query"], json_media["query"],
json_media.get("variables"), json_media.get("variables"),
json_media.get("operationName"), json_media.get("operationName"),
) )
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
# if "query" in req.media("form"):
# return req.media("form")["query"], None, None
# if "q" in req.media("form"):
# return req.media("form")["q"], None, None
# Support query/q in params. # Support query/q in params.
if "query" in req.params: if "query" in req.params:
return req.params["query"], None, None return req.params["query"], None, None
@@ -29,38 +37,34 @@ class GraphQLView:
return req.params["q"], None, None return req.params["q"], None, None
# Otherwise, the request text is used (typical). # Otherwise, the request text is used (typical).
return await req.text, None, None # TODO: Make some assertions about content-type here.
return req.text, None, None
async def graphql_response(self, req, resp): async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html") show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql: if show_graphiql:
resp.content = self.api.templates.render_string( resp.content = self.api.templates.render_string(
GRAPHIQL, endpoint=req.url.path GRAPHIQL, endpoint=req.url.path
) )
return None return
query, variables, operation_name = await self._resolve_graphql_query(req, resp)
if query is None:
return None
query, variables, operation_name = await self._resolve_graphql_query(req)
context = {"request": req, "response": resp} context = {"request": req, "response": resp}
result = self.schema.execute( result = schema.execute(
query, variables=variables, operation_name=operation_name, context=context query, variables=variables, operation_name=operation_name, context=context
) )
result, status_code = encode_execution_results(
response_data = {} [result],
if result.errors: is_batch=False,
response_data["errors"] = [{"message": str(e)} for e in result.errors] format_error=default_format_error,
if result.data is not None: encode=partial(json_encode, pretty=False),
response_data["data"] = result.data )
resp.media = json.loads(result)
resp.media = response_data return (query, result, status_code)
status_code = 200 if not result.errors else 400
return (query, json.dumps(response_data), status_code)
async def on_request(self, req, resp): async def on_request(self, req, resp):
await self.graphql_response(req, resp) await self.graphql_response(req, resp, self.schema)
async def __call__(self, req, resp): async def __call__(self, req, resp):
await self.on_request(req, resp) await self.on_request(req, resp)
+121 -10
View File
@@ -1,8 +1,13 @@
# ruff: noqa: E501
GRAPHIQL = """ GRAPHIQL = """
{% set GRAPHIQL_VERSION = '3.0.6' %} {% set GRAPHIQL_VERSION = '0.12.0' %}
{% set REACT_VERSION = '18.2.0' %}
<!--
* Copyright (c) Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
-->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -17,17 +22,123 @@ GRAPHIQL = """
height: 100vh; height: 100vh;
} }
</style> </style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.css" rel="stylesheet"/>
<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bunder.
-->
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.css" rel="stylesheet"/>
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
</head> </head>
<body> <body>
<div id="graphiql">Loading...</div> <div id="graphiql">Loading...</div>
<script crossorigin src="//cdn.jsdelivr.net/npm/react@{{ REACT_VERSION }}/umd/react.production.min.js"></script>
<script crossorigin src="//cdn.jsdelivr.net/npm/react-dom@{{ REACT_VERSION }}/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.js"></script>
<script> <script>
const fetcher = GraphiQL.createFetcher({ url: '{{ endpoint }}' });
const root = ReactDOM.createRoot(document.getElementById('graphiql')); /**
root.render(React.createElement(GraphiQL, { fetcher: fetcher })); * This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams) {
// This example expects a GraphQL server at the path /graphql.
// Change this to point wherever you host your GraphQL server.
return fetch('{{ endpoint }}', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
</script> </script>
</body> </body>
</html> </html>
-24
View File
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>{{ title }} {{ version }}</title>
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/@stoplight/elements/styles.min.css"
/>
</head>
<body>
<elements-api
apiDescriptionUrl="{{ schema_url }}"
router="hash"
layout="sidebar"
/>
</body>
</html>
-16
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<!-- Important: must specify -->
<html>
<head>
<title>{{ title }} {{ version }}</title>
<meta charset="utf-8" />
<!-- Important: rapi-doc uses utf8 characters -->
<script
type="module"
src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"
></script>
</head>
<body>
<rapi-doc spec-url="{{ schema_url }}" show-header="false"> </rapi-doc>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ title }} {{ version }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="{{ schema_url }}"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{ title }} {{ version }}</title>
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/swagger-ui-dist/swagger-ui.css"
/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function () {
const ui = SwaggerUIBundle({
url: "{{ schema_url }}",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "BaseLayout",
});
};
</script>
</body>
</html>
@@ -1,14 +1,18 @@
import os
from pathlib import Path from pathlib import Path
import apistar
import jinja2
import yaml
from apispec import APISpec, yaml_utils from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin from apispec.ext.marshmallow import MarshmallowPlugin
from responder.statics import DEFAULT_API_THEME
from responder.staticfiles import StaticFiles
from responder import status_codes from responder import status_codes
from responder.statics import API_THEMES, DEFAULT_OPENAPI_THEME
from responder.templates import Templates
class OpenAPISchema: class Schema:
def __init__( def __init__(
self, self,
app, app,
@@ -18,12 +22,11 @@ class OpenAPISchema:
description=None, description=None,
terms_of_service=None, terms_of_service=None,
contact=None, contact=None,
license=None, # noqa: A002 license=None,
openapi=None, openapi=None,
openapi_route="/schema.yml", openapi_route="/schema.yml",
docs_route="/docs/", docs_route="/docs/",
static_route="/static", static_route="/static",
openapi_theme=DEFAULT_OPENAPI_THEME,
): ):
self.app = app self.app = app
self.schemas = {} self.schemas = {}
@@ -37,9 +40,7 @@ class OpenAPISchema:
self.openapi_version = openapi self.openapi_version = openapi
self.openapi_route = openapi_route self.openapi_route = openapi_route
self.docs_theme = ( self.docs_theme = DEFAULT_API_THEME
openapi_theme if openapi_theme in API_THEMES else DEFAULT_OPENAPI_THEME
)
self.docs_route = docs_route self.docs_route = docs_route
self.plugins = [MarshmallowPlugin()] if plugins is None else plugins self.plugins = [MarshmallowPlugin()] if plugins is None else plugins
@@ -50,13 +51,17 @@ class OpenAPISchema:
if self.docs_route is not None: if self.docs_route is not None:
self.app.add_route(self.docs_route, self.docs_response) self.app.add_route(self.docs_route, self.docs_response)
theme_path = (Path(__file__).parent / "docs").resolve() theme_path = (
self.templates = Templates(directory=theme_path) Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
).resolve()
self.static_route = static_route self.static_route = static_route
self.app.static_app.add_directory(theme_path)
@property @property
def _apispec(self): def _apispec(self):
info = {} info = {}
if self.description is not None: if self.description is not None:
info["description"] = self.description info["description"] = self.description
@@ -77,7 +82,9 @@ class OpenAPISchema:
for route in self.app.router.routes: for route in self.app.router.routes:
if route.description: if route.description:
operations = yaml_utils.load_operations_from_docstring(route.description) operations = yaml_utils.load_operations_from_docstring(
route.description
)
spec.path(path=route.route, operations=operations) spec.path(path=route.route, operations=operations)
for name, schema in self.schemas.items(): for name, schema in self.schemas.items():
@@ -90,7 +97,7 @@ class OpenAPISchema:
return self._apispec.to_yaml() return self._apispec.to_yaml()
def add_schema(self, name, schema, check_existing=True): def add_schema(self, name, schema, check_existing=True):
"""Adds a marshmallow schema to the API specification.""" """Adds a mashmallow schema to the API specification."""
if check_existing: if check_existing:
assert name not in self.schemas assert name not in self.schemas
@@ -117,10 +124,25 @@ class OpenAPISchema:
@property @property
def docs(self): def docs(self):
return self.templates.render(
f"{self.docs_theme}.html", loader = jinja2.PrefixLoader(
title=self.title, {
version=self.version, self.docs_theme: jinja2.PackageLoader(
"apistar", os.path.join("themes", self.docs_theme, "templates")
)
}
)
env = jinja2.Environment(autoescape=True, loader=loader)
document = apistar.document.Document()
document.content = yaml.safe_load(self.openapi)
template = env.get_template("/".join([self.docs_theme, "index.html"]))
return template.render(
document=document,
langs=["javascript", "python"],
code_style=None,
static_url=self.static_url,
schema_url="/schema.yml", schema_url="/schema.yml",
) )
@@ -133,6 +155,6 @@ class OpenAPISchema:
resp.html = self.docs resp.html = self.docs
def schema_response(self, req, resp): def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200 # type: ignore[attr-defined] resp.status_code = status_codes.HTTP_200
resp.headers["Content-Type"] = "application/x-yaml" resp.headers["Content-Type"] = "application/x-yaml"
resp.content = self.openapi resp.content = self.openapi
+43 -101
View File
@@ -1,142 +1,84 @@
from __future__ import annotations
import json import json
from urllib.parse import urlencode from urllib.parse import urlencode
import yaml import yaml
from python_multipart import MultipartParser from requests_toolbelt.multipart import decoder
from .models import QueryDict from .models import QueryDict
class _PartData:
__slots__ = ("headers", "body", "header_field")
def __init__(self):
self.headers: dict[str, str] = {}
self.body = b""
self.header_field = ""
def _parse_multipart(content: bytes, content_type: str) -> list[_PartData]:
"""Parse multipart form data into a list of parts with headers and body."""
boundary = None
for segment in content_type.split(";"):
segment = segment.strip()
if segment.startswith("boundary="):
boundary = segment.split("=", 1)[1].strip('"')
break
if boundary is None:
return []
parts: list[_PartData] = []
current: list[_PartData | None] = [None]
def on_part_begin():
current[0] = _PartData()
def on_part_data(data, start, end):
current[0].body += data[start:end] # type: ignore[union-attr]
def on_header_field(data, start, end):
current[0].header_field = data[start:end].decode("utf-8") # type: ignore[union-attr]
def on_header_value(data, start, end):
part = current[0]
assert part is not None
part.headers[part.header_field] = data[start:end].decode("utf-8")
def on_part_end():
parts.append(current[0]) # type: ignore[arg-type]
parser = MultipartParser(
boundary.encode(),
{ # type: ignore[arg-type]
"on_part_begin": on_part_begin,
"on_part_data": on_part_data,
"on_header_field": on_header_field,
"on_header_value": on_header_value,
"on_part_end": on_part_end,
},
)
parser.write(content)
parser.finalize()
return parts
async def format_form(r, encode=False): async def format_form(r, encode=False):
if encode: if encode:
return None pass
if "multipart/form-data" in r.headers.get("Content-Type"): elif "multipart/form-data" in r.headers.get("Content-Type"):
parts = _parse_multipart(await r.content, r.mimetype) decode = decoder.MultipartDecoder(await r.content, r.mimetype)
queries = [] querys = list()
for part in parts: for part in decode.parts:
header = part.headers.get("Content-Disposition", "") header = part.headers.get(b"Content-Disposition").decode("utf-8")
text = part.body.decode("utf-8") text = part.text
for section in [h.strip() for h in header.split(";")]: for section in [h.strip() for h in header.split(";")]:
split = section.split("=") split = section.split("=")
if len(split) > 1: if len(split) > 1:
key = split[1] key = split[1]
key = key[1:-1] key = key[1:-1]
queries.append((key, text)) querys.append((key, text))
content = urlencode(queries) content = urlencode(querys)
return QueryDict(content) return QueryDict(content)
return QueryDict(await r.text) else:
return QueryDict(await r.text)
async def format_yaml(r, encode=False): async def format_yaml(r, encode=False):
if encode: if encode:
r.headers.update({"Content-Type": "application/x-yaml"}) r.headers.update({"Content-Type": "application/x-yaml"})
return yaml.safe_dump(r.media) return yaml.safe_dump(r.media)
return yaml.safe_load(await r.content) else:
return yaml.safe_load(await r.content)
async def format_json(r, encode=False): async def format_json(r, encode=False):
if encode: if encode:
r.headers.update({"Content-Type": "application/json"}) r.headers.update({"Content-Type": "application/json"})
return json.dumps(r.media) return json.dumps(r.media)
return json.loads(await r.content) else:
return json.loads(await r.content)
async def format_files(r, encode=False): async def format_files(r, encode=False):
if encode: if encode:
return None pass
parts = _parse_multipart(await r.content, r.mimetype) else:
dump = {} decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
for part in parts: dump = {}
header = part.headers.get("Content-Disposition", "") for part in decoded.parts:
mimetype = part.headers.get("Content-Type", None) header = part.headers[b"Content-Disposition"].decode("utf-8")
filename = None mimetype = part.headers.get(b"Content-Type", None)
formname = None filename = None
for section in [h.strip() for h in header.split(";")]: for section in [h.strip() for h in header.split(";")]:
split = section.split("=") split = section.split("=")
if len(split) > 1: if len(split) > 1:
key = split[0] key = split[0]
value = split[1] value = split[1]
value = value[1:-1]
if key == "filename": value = value[1:-1]
filename = value
elif key == "name":
formname = value
if formname is None: if key == "filename":
continue filename = value
elif key == "name":
formname = value
if mimetype is None: if mimetype is None:
dump[formname] = part.body dump[formname] = part.content
else: else:
dump[formname] = { dump[formname] = {
"filename": filename, "filename": filename,
"content": part.body, "content": part.content,
"content-type": mimetype, "content-type": mimetype.decode("utf-8"),
} }
return dump return dump
def get_formats(): def get_formats():
+41 -127
View File
@@ -1,51 +1,29 @@
from __future__ import annotations
import functools import functools
import io
import inspect import inspect
from collections.abc import Callable import json
import gzip
from urllib.parse import parse_qs
from base64 import b64decode
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from urllib.parse import parse_qs, urlparse
__all__ = ["Request", "Response", "QueryDict"] import chardet
import rfc3986
import graphene
import yaml
try: from requests.structures import CaseInsensitiveDict
import chardet from requests.cookies import RequestsCookieJar
except ImportError:
chardet = None # type: ignore[assignment] from starlette.datastructures import MutableHeaders
from starlette.requests import Request as StarletteRequest from starlette.requests import Request as StarletteRequest, State
from starlette.requests import State
from starlette.responses import ( from starlette.responses import (
Response as StarletteResponse, Response as StarletteResponse,
)
from starlette.responses import (
StreamingResponse as StarletteStreamingResponse, StreamingResponse as StarletteStreamingResponse,
) )
from .status_codes import HTTP_200, HTTP_301
from .statics import DEFAULT_ENCODING from .statics import DEFAULT_ENCODING
from .status_codes import HTTP_301 # type: ignore[attr-defined]
class CaseInsensitiveDict(dict):
"""A case-insensitive dict for HTTP headers."""
def __setitem__(self, key, value):
super().__setitem__(key.lower(), value)
def __getitem__(self, key):
return super().__getitem__(key.lower())
def __contains__(self, key):
return super().__contains__(key.lower())
def get(self, key, default=None):
return super().get(key.lower(), default)
def update(self, other=None, **kwargs):
if other:
for key, value in other.items():
self[key] = value
for key, value in kwargs.items():
self[key] = value
class QueryDict(dict): class QueryDict(dict):
@@ -134,7 +112,7 @@ class Request:
self.api = api self.api = api
self._content = None self._content = None
headers: CaseInsensitiveDict = CaseInsensitiveDict() headers = CaseInsensitiveDict()
for key, value in self._starlette.headers.items(): for key, value in self._starlette.headers.items():
headers[key] = value headers[key] = value
@@ -155,11 +133,6 @@ class Request:
def mimetype(self): def mimetype(self):
return self.headers.get("Content-Type", "") return self.headers.get("Content-Type", "")
@property
def is_json(self):
"""Returns ``True`` if the request content type is JSON."""
return "json" in self.mimetype
@property @property
def method(self): def method(self):
"""The incoming HTTP method used for the request, lower-cased.""" """The incoming HTTP method used for the request, lower-cased."""
@@ -173,20 +146,20 @@ class Request:
@property @property
def url(self): def url(self):
"""The parsed URL of the Request.""" """The parsed URL of the Request."""
return urlparse(self.full_url) return rfc3986.urlparse(self.full_url)
@property @property
def cookies(self): def cookies(self):
"""The cookies sent in the Request, as a dictionary.""" """The cookies sent in the Request, as a dictionary."""
if self._cookies is None: if self._cookies is None:
cookies = {} cookies = RequestsCookieJar()
cookie_header = self.headers.get("Cookie", "") cookie_header = self.headers.get("Cookie", "")
bc: SimpleCookie = SimpleCookie(cookie_header) bc = SimpleCookie(cookie_header)
for key, morsel in bc.items(): for key, morsel in bc.items():
cookies[key] = morsel.value cookies[key] = morsel.value
self._cookies = cookies self._cookies = cookies.get_dict()
return self._cookies return self._cookies
@@ -198,16 +171,6 @@ class Request:
except AttributeError: except AttributeError:
return QueryDict({}) return QueryDict({})
@property
def path_params(self) -> dict:
"""The path parameters extracted from the URL route."""
return self._starlette.path_params
@property
def client(self):
"""The client's address as a (host, port) named tuple, or None."""
return self._starlette.client
@property @property
def state(self) -> State: def state(self) -> State:
""" """
@@ -250,23 +213,16 @@ class Request:
async def declared_encoding(self): async def declared_encoding(self):
if "Encoding" in self.headers: if "Encoding" in self.headers:
return self.headers["Encoding"] return self.headers["Encoding"]
return None
@property @property
async def apparent_encoding(self): async def apparent_encoding(self):
"""The apparent encoding, detected automatically. Must be awaited. """The apparent encoding, provided by the chardet library. Must be awaited."""
Uses chardet for detection if installed, otherwise falls back to UTF-8.
"""
declared_encoding = await self.declared_encoding declared_encoding = await self.declared_encoding
if declared_encoding: if declared_encoding:
return declared_encoding return declared_encoding
if chardet is not None: return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
return DEFAULT_ENCODING
@property @property
def is_secure(self): def is_secure(self):
@@ -276,31 +232,20 @@ class Request:
"""Returns ``True`` if the incoming Request accepts the given ``content_type``.""" """Returns ``True`` if the incoming Request accepts the given ``content_type``."""
return content_type in self.headers.get("Accept", []) return content_type in self.headers.get("Accept", [])
async def media(self, format: str | Callable = None): # noqa: A002 async def media(self, format=None):
"""Renders incoming json/yaml/form data as Python objects. Must be awaited. """Renders incoming json/yaml/form data as Python objects. Must be awaited.
:param format: The name of the format being used. :param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
Alternatively, accepts a custom callable for the format type.
""" """
if format is None: if format is None:
format = "yaml" if "yaml" in self.mimetype or "" else "json" # noqa: A001 format = "yaml" if "yaml" in self.mimetype or "" else "json"
format = "form" if "form" in self.mimetype or "" else format # noqa: A001 format = "form" if "form" in self.mimetype or "" else format
formatter: Callable
if isinstance(format, str):
try:
formatter = self.formats[format]
except KeyError as ex:
raise ValueError(f"Unable to process data in '{format}' format") from ex
elif callable(format):
formatter = format
if format in self.formats:
return await self.formats[format](self)
else: else:
raise TypeError(f"Invalid 'format' argument: {format}") return await format(self)
return await formatter(self)
def content_setter(mimetype): def content_setter(mimetype):
@@ -334,22 +279,22 @@ class Response:
def __init__(self, req, *, formats): def __init__(self, req, *, formats):
self.req = req self.req = req
#: The HTTP Status Code to use for the Response. self.status_code = None #: The HTTP Status Code to use for the Response.
self.status_code: int | None = None
self.content = None #: A bytes representation of the response body. self.content = None #: A bytes representation of the response body.
self.mimetype = None self.mimetype = None
self.encoding = DEFAULT_ENCODING self.encoding = DEFAULT_ENCODING
self.media = None #: A Python object that will be content-negotiated and self.media = None #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
#: sent back to the client. Typically, in JSON formatting.
self._stream = None self._stream = None
self.headers = {} #: A Python dictionary of ``{key: value}``, self.headers = (
#: representing the headers of the response. {}
) #: A Python dictionary of ``{key: value}``, representing the headers of the response.
self.formats = formats self.formats = formats
self.cookies: SimpleCookie = SimpleCookie() #: The cookies set in the Response self.cookies = SimpleCookie() #: The cookies set in the Response
self.session = ( self.session = (
req.session req.session
) #: The cookie-based session data, in dict form, to add to the Response. ) #: The cookie-based session data, in dict form, to add to the Response.
# Property or func/dec
def stream(self, func, *args, **kwargs): def stream(self, func, *args, **kwargs):
assert inspect.isasyncgenfunction(func) assert inspect.isasyncgenfunction(func)
@@ -357,25 +302,6 @@ class Response:
return func return func
def file(self, path, *, content_type=None):
"""Serve a file from disk as the response.
:param path: Path to the file to serve.
:param content_type: Optional MIME type override.
"""
from pathlib import Path
path = Path(path)
self.content = path.read_bytes()
if content_type:
self.mimetype = content_type
else:
import mimetypes
guessed = mimetypes.guess_type(str(path))[0]
self.mimetype = guessed or "application/octet-stream"
def redirect(self, location, *, set_text=True, status_code=HTTP_301): def redirect(self, location, *, set_text=True, status_code=HTTP_301):
self.status_code = status_code self.status_code = status_code
if set_text: if set_text:
@@ -394,13 +320,12 @@ class Response:
headers["Content-Type"] = self.mimetype headers["Content-Type"] = self.mimetype
if self.mimetype == "text/plain" and self.encoding is not None: if self.mimetype == "text/plain" and self.encoding is not None:
headers["Encoding"] = self.encoding headers["Encoding"] = self.encoding
if isinstance(content, str): content = content.encode(self.encoding)
content = content.encode(self.encoding)
return (content, headers) return (content, headers)
for format_ in self.formats: for format in self.formats:
if self.req.accepts(format_): if self.req.accepts(format):
return (await self.formats[format_](self, encode=True)), {} return (await self.formats[format](self, encode=True)), {}
# Default to JSON anyway. # Default to JSON anyway.
return ( return (
@@ -444,23 +369,12 @@ class Response:
if self.headers: if self.headers:
headers.update(self.headers) headers.update(self.headers)
response_cls: type[StarletteResponse] | type[StarletteStreamingResponse]
if self._stream is not None: if self._stream is not None:
response_cls = StarletteStreamingResponse response_cls = StarletteStreamingResponse
else: else:
response_cls = StarletteResponse response_cls = StarletteResponse
response = response_cls(body, status_code=self.status_code_safe, headers=headers) response = response_cls(body, status_code=self.status_code, headers=headers)
self._prepare_cookies(response) self._prepare_cookies(response)
await response(scope, receive, send) await response(scope, receive, send)
@property
def ok(self):
return 200 <= self.status_code_safe < 300
@property
def status_code_safe(self) -> int:
if self.status_code is None:
raise RuntimeError("HTTP status code has not been defined")
return self.status_code
+37 -104
View File
@@ -1,28 +1,24 @@
import asyncio import asyncio
import inspect import json
import re import re
import traceback import inspect
from collections import defaultdict
__all__ = ["Route", "WebSocketRoute", "Router"]
from starlette.routing import Lifespan
from starlette.middleware.wsgi import WSGIMiddleware
from starlette.websockets import WebSocket, WebSocketClose
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.types import ASGIApp
from starlette.websockets import WebSocket, WebSocketClose
from .models import Request, Response
from . import status_codes from . import status_codes
from .formats import get_formats from .formats import get_formats
from .models import Request, Response from .statics import DEFAULT_SESSION_COOKIE
_UUID_RE = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
_CONVERTORS = { _CONVERTORS = {
"int": (int, r"\d+"), "int": (int, r"\d+"),
"str": (str, r"[^/]+"), "str": (str, r"[^/]+"),
"float": (float, r"\d+(.\d+)?"), "float": (float, r"\d+(.\d+)?"),
"path": (str, r".+"),
"uuid": (str, _UUID_RE),
} }
PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}") PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
@@ -36,9 +32,9 @@ def compile_path(path):
for match in PARAM_RE.finditer(path): for match in PARAM_RE.finditer(path):
param_name, convertor_type = match.groups(default="str") param_name, convertor_type = match.groups(default="str")
convertor_type = convertor_type.lstrip(":") convertor_type = convertor_type.lstrip(":")
assert convertor_type in _CONVERTORS.keys(), ( assert (
f"Unknown path convertor '{convertor_type}'" convertor_type in _CONVERTORS.keys()
) ), f"Unknown path convertor '{convertor_type}'"
convertor, convertor_re = _CONVERTORS[convertor_type] convertor, convertor_re = _CONVERTORS[convertor_type]
path_re += path[idx : match.start()] path_re += path[idx : match.start()]
@@ -62,22 +58,19 @@ class BaseRoute:
class Route(BaseRoute): class Route(BaseRoute):
def __init__(self, route, endpoint, *, before_request=False, methods=None): def __init__(self, route, endpoint, *, before_request=False):
assert route.startswith("/"), "Route path must start with '/'" assert route.startswith("/"), "Route path must start with '/'"
self.route = route self.route = route
self.endpoint = endpoint self.endpoint = endpoint
self.before_request = before_request self.before_request = before_request
self.methods = {m.upper() for m in methods} if methods else None
self.path_re, self.param_convertors = compile_path(route) self.path_re, self.param_convertors = compile_path(route)
# Strip type annotations for URL generation (e.g. {id:int} -> {id})
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self): def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>" return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params): def url(self, **params):
return self._url_template.format(**params) return self.route.format(**params)
@property @property
def endpoint_name(self): def endpoint_name(self):
@@ -91,9 +84,6 @@ class Route(BaseRoute):
if scope["type"] != "http": if scope["type"] != "http":
return False, {} return False, {}
if self.methods and scope.get("method", "").upper() not in self.methods:
return False, {}
path = scope["path"] path = scope["path"]
match = self.path_re.match(path) match = self.path_re.match(path)
@@ -118,10 +108,6 @@ class Route(BaseRoute):
await before_request(request, response) await before_request(request, response)
else: else:
await run_in_threadpool(before_request, request, response) await run_in_threadpool(before_request, request, response)
# If a before_request hook set a status code, short-circuit
if response.status_code is not None:
await response(scope, receive, send)
return
views = [] views = []
@@ -135,14 +121,14 @@ class Route(BaseRoute):
try: try:
view = getattr(endpoint, method_name) view = getattr(endpoint, method_name)
views.append(view) views.append(view)
except AttributeError as ex: except AttributeError:
if on_request is None: if on_request is None:
raise HTTPException(status_code=status_codes.HTTP_405) from ex # type: ignore[attr-defined] raise HTTPException(status_code=status_codes.HTTP_405)
else: else:
views.append(self.endpoint) views.append(self.endpoint)
for view in views: for view in views:
# Check __call__ for class-based views (e.g. GraphQL) # "Monckey patch" for graphql: explicitly checking __call__
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction( if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
view.__call__ view.__call__
): ):
@@ -151,11 +137,12 @@ class Route(BaseRoute):
await run_in_threadpool(view, request, response, **path_params) await run_in_threadpool(view, request, response, **path_params)
if response.status_code is None: if response.status_code is None:
response.status_code = status_codes.HTTP_200 # type: ignore[attr-defined] response.status_code = status_codes.HTTP_200
await response(scope, receive, send) await response(scope, receive, send)
def __eq__(self, other): def __eq__(self, other):
# [TODO] compare to str ?
return self.route == other.route and self.endpoint == other.endpoint return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self): def __hash__(self):
@@ -170,13 +157,12 @@ class WebSocketRoute(BaseRoute):
self.before_request = before_request self.before_request = before_request
self.path_re, self.param_convertors = compile_path(route) self.path_re, self.param_convertors = compile_path(route)
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self): def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>" return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params): def url(self, **params):
return self._url_template.format(**params) return self.route.format(**params)
@property @property
def endpoint_name(self): def endpoint_name(self):
@@ -212,6 +198,7 @@ class WebSocketRoute(BaseRoute):
await self.endpoint(ws) await self.endpoint(ws)
def __eq__(self, other): def __eq__(self, other):
# [TODO] compare to str ?
return self.route == other.route and self.endpoint == other.endpoint return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self): def __hash__(self):
@@ -219,20 +206,17 @@ class WebSocketRoute(BaseRoute):
class Router: class Router:
def __init__( def __init__(self, routes=None, default_response=None, before_requests=None):
self, routes=None, default_response=None, before_requests=None, lifespan=None
):
self.routes = [] if routes is None else list(routes) self.routes = [] if routes is None else list(routes)
# [TODO] Make its own router
self.apps: dict[str, ASGIApp] = {} self.apps = {}
self.default_endpoint = ( self.default_endpoint = (
self.default_response if default_response is None else default_response self.default_response if default_response is None else default_response
) )
self.lifespan_handler = Lifespan()
self.before_requests = ( self.before_requests = (
{"http": [], "ws": []} if before_requests is None else before_requests {"http": [], "ws": []} if before_requests is None else before_requests
) )
self.events = defaultdict(list)
self._lifespan_handler = lifespan
def add_route( def add_route(
self, self,
@@ -243,13 +227,11 @@ class Router:
websocket=False, websocket=False,
before_request=False, before_request=False,
check_existing=False, check_existing=False,
methods=None,
): ):
"""Adds a route to the router. """ Adds a route to the router.
:param route: A string representation of the route :param route: A string representation of the route
:param endpoint: The endpoint for the route -- can be callable, or class. :param endpoint: The endpoint for the route -- can be callable, or class.
:param default: If ``True``, all unknown requests will route to this view. :param default: If ``True``, all unknown requests will route to this view.
:param methods: Optional list of HTTP methods (e.g. ["GET", "POST"]).
""" """
if before_request: if before_request:
if websocket: if websocket:
@@ -259,9 +241,9 @@ class Router:
return return
if check_existing: if check_existing:
assert not self.routes or route not in (item.route for item in self.routes), ( assert not self.routes or route not in (
f"Route '{route}' already exists" item.route for item in self.routes
) ), f"Route '{route}' already exists"
if default: if default:
self.default_endpoint = endpoint self.default_endpoint = endpoint
@@ -269,27 +251,14 @@ class Router:
if websocket: if websocket:
route = WebSocketRoute(route, endpoint) route = WebSocketRoute(route, endpoint)
else: else:
route = Route(route, endpoint, methods=methods) route = Route(route, endpoint)
self.routes.append(route) self.routes.append(route)
def mount(self, route, app): def mount(self, route, app):
"""Mounts ASGI / WSGI applications at a given route""" """Mounts ASGI / WSGI applications at a given route
self.apps.update({route: app}) """
self.apps.update(route, app)
def add_event_handler(self, event_type, handler):
assert event_type in (
"startup",
"shutdown",
), f"Only 'startup' and 'shutdown' events are supported, not {event_type}."
self.events[event_type].append(handler)
async def trigger_event(self, event_type):
for handler in self.events.get(event_type, []):
if asyncio.iscoroutinefunction(handler):
await handler()
else:
handler()
def before_request(self, endpoint, websocket=False): def before_request(self, endpoint, websocket=False):
if websocket: if websocket:
@@ -298,6 +267,7 @@ class Router:
self.before_requests.setdefault("http", []).append(endpoint) self.before_requests.setdefault("http", []).append(endpoint)
def url_for(self, endpoint, **params): def url_for(self, endpoint, **params):
# TODO: Check for params
for route in self.routes: for route in self.routes:
if endpoint in (route.endpoint, route.endpoint.__name__): if endpoint in (route.endpoint, route.endpoint.__name__):
return route.url(**params) return route.url(**params)
@@ -306,13 +276,13 @@ class Router:
async def default_response(self, scope, receive, send): async def default_response(self, scope, receive, send):
if scope["type"] == "websocket": if scope["type"] == "websocket":
websocket_close = WebSocketClose() websocket_close = WebSocketClose()
await websocket_close(scope, receive, send) await websocket_close(receive, send)
return return
request = Request(scope, receive) request = Request(scope, receive)
response = Response(request, formats=get_formats()) # noqa: F841 response = Response(request, formats=get_formats())
raise HTTPException(status_code=status_codes.HTTP_404) # type: ignore[attr-defined] raise HTTPException(status_code=status_codes.HTTP_404)
def _resolve_route(self, scope): def _resolve_route(self, scope):
for route in self.routes: for route in self.routes:
@@ -322,46 +292,11 @@ class Router:
return route return route
return None return None
async def lifespan(self, scope, receive, send):
message = await receive()
assert message["type"] == "lifespan.startup"
if self._lifespan_handler is not None:
# Modern lifespan context manager pattern
try:
ctx = self._lifespan_handler(scope.get("app"))
await ctx.__aenter__()
except BaseException:
msg = traceback.format_exc()
await send({"type": "lifespan.startup.failed", "message": msg})
raise
await send({"type": "lifespan.startup.complete"})
message = await receive()
assert message["type"] == "lifespan.shutdown"
await ctx.__aexit__(None, None, None)
else:
# Legacy on_event("startup") / on_event("shutdown") pattern
try:
await self.trigger_event("startup")
except BaseException:
msg = traceback.format_exc()
await send({"type": "lifespan.startup.failed", "message": msg})
raise
await send({"type": "lifespan.startup.complete"})
message = await receive()
assert message["type"] == "lifespan.shutdown"
await self.trigger_event("shutdown")
await send({"type": "lifespan.shutdown.complete"})
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
assert scope["type"] in ("http", "websocket", "lifespan") assert scope["type"] in ("http", "websocket", "lifespan")
if scope["type"] == "lifespan": if scope["type"] == "lifespan":
await self.lifespan(scope, receive, send) await self.lifespan_handler(scope, receive, send)
return return
path = scope["path"] path = scope["path"]
@@ -385,10 +320,8 @@ class Router:
await app(scope, receive, send) await app(scope, receive, send)
return return
except TypeError: except TypeError:
from a2wsgi import WSGIMiddleware
app = WSGIMiddleware(app) app = WSGIMiddleware(app)
await app(scope, receive, send) await app(scope, receive, send)
return return
await self.default_endpoint(scope, receive, send) await self.default_response(scope, receive, send)
+13 -3
View File
@@ -1,8 +1,18 @@
from starlette.staticfiles import StaticFiles as StarletteStaticFiles import typing
from starlette.staticfiles import StaticFiles
class StaticFiles(StarletteStaticFiles): class StaticFiles(StaticFiles):
"""Extension to Starlette's StaticFiles with support for multiple directories.""" """I've created an issue to disccuss allowing multiple directories in starletter's `StaticFiles`.
https://github.com/encode/starlette/issues/625
I've also made a PR to add this method to starlette StaticFiles
Once accepted we will remove this.
https://github.com/encode/starlette/pull/626
"""
def add_directory(self, directory: str) -> None: def add_directory(self, directory: str) -> None:
self.all_directories = [*self.all_directories, *self.get_directories(directory)] self.all_directories = [*self.all_directories, *self.get_directories(directory)]
+2 -3
View File
@@ -1,8 +1,7 @@
API_THEMES = ["elements", "rapidoc", "redoc", "swagger_ui"]
DEFAULT_ENCODING = "utf-8" DEFAULT_ENCODING = "utf-8"
DEFAULT_OPENAPI_THEME = "swagger_ui" DEFAULT_API_THEME = "swaggerui"
DEFAULT_SESSION_COOKIE = "Responder-Session" DEFAULT_SESSION_COOKIE = "Responder-Session"
DEFAULT_SECRET_KEY = "NOTASECRET" # noqa: S105 DEFAULT_SECRET_KEY = "NOTASECRET"
DEFAULT_CORS_PARAMS = { DEFAULT_CORS_PARAMS = {
"allow_origins": (), "allow_origins": (),
+7 -1
View File
@@ -1,3 +1,5 @@
# from: https://github.com/requests/requests/blob/master/requests/status_codes.py
codes = { codes = {
# Informational. # Informational.
100: ("continue",), 100: ("continue",),
@@ -24,7 +26,11 @@ codes = {
305: ("use_proxy",), 305: ("use_proxy",),
306: ("switch_proxy",), 306: ("switch_proxy",),
307: ("temporary_redirect", "temporary_moved", "temporary"), 307: ("temporary_redirect", "temporary_moved", "temporary"),
308: ("permanent_redirect",), 308: (
"permanent_redirect",
"resume_incomplete",
"resume",
), # These 2 to be removed in 3.0
# Client Error. # Client Error.
400: ("bad_request", "bad"), 400: ("bad_request", "bad"),
401: ("unauthorized",), 401: ("unauthorized",),
+3 -5
View File
@@ -2,8 +2,6 @@ from contextlib import contextmanager
import jinja2 import jinja2
__all__ = ["Templates"]
class Templates: class Templates:
def __init__( def __init__(
@@ -12,7 +10,7 @@ class Templates:
self.directory = directory self.directory = directory
self._env = jinja2.Environment( self._env = jinja2.Environment(
loader=jinja2.FileSystemLoader([str(self.directory)]), loader=jinja2.FileSystemLoader([str(self.directory)]),
autoescape=autoescape, # noqa: S701 autoescape=autoescape,
enable_async=enable_async, enable_async=enable_async,
) )
self.default_context = {} if context is None else {**context} self.default_context = {} if context is None else {**context}
@@ -35,7 +33,7 @@ class Templates:
:param template: The filename of the jinja2 template. :param template: The filename of the jinja2 template.
:param **kwargs: Data to pass into the template. :param **kwargs: Data to pass into the template.
:param **kwargs: Data to pass into the template. :param **kwargs: Data to pass into the template.
""" # noqa: E501 """
return self.get_template(template).render(*args, **kwargs) return self.get_template(template).render(*args, **kwargs)
@contextmanager @contextmanager
@@ -56,6 +54,6 @@ class Templates:
:param source: The template to use. :param source: The template to use.
:param *args, **kwargs: Data to pass into the template. :param *args, **kwargs: Data to pass into the template.
:param **kwargs: Data to pass into the template. :param **kwargs: Data to pass into the template.
""" # noqa: E501 """
template = self._env.from_string(source) template = self._env.from_string(source)
return template.render(*args, **kwargs) return template.render(*args, **kwargs)
View File
-242
View File
@@ -1,242 +0,0 @@
# ruff: noqa: S603 # Subprocess call - output not captured
# ruff: noqa: S607 # Starting a process with a partial executable path
# Security considerations for subprocess usage:
# 1. Only execute the 'responder' binary from PATH
# 2. Validate all user inputs before passing to subprocess
# 3. Use Path.resolve() to prevent path traversal
import functools
import logging
import os
import shutil
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
logger = logging.getLogger(__name__)
class ResponderProgram:
"""
Utility class for managing Responder program execution.
This class provides methods for:
- Locating the responder executable in PATH
- Building frontend assets using npm
Example:
>>> program_path = ResponderProgram.path()
>>> build_status = ResponderProgram.build(Path("app_dir"))
"""
@staticmethod
@functools.lru_cache(maxsize=None)
def path():
name = "responder"
if sys.platform == "win32":
name = "responder.exe"
program = shutil.which(name)
if program is None:
paths = os.environ.get("PATH", "").split(os.pathsep)
raise RuntimeError(
f"Could not find '{name}' executable in PATH. "
f"Please install Responder with 'pip install --upgrade responder'. "
f"Searched in: {', '.join(paths)}"
)
logger.debug(f"Found responder program: {program}")
return program
@classmethod
def build(cls, path: Path) -> int:
"""
Invoke `responder build` command.
Args:
path: Path to the application to build
Returns:
int: The return code from the build process
Raises:
ValueError: If the path is invalid
RuntimeError: If the responder executable is not found
subprocess.SubprocessError: If the build process fails
"""
if not isinstance(path, Path):
raise ValueError(f"Expected a Path object, got {type(path).__name__}")
if not path.exists():
raise ValueError(f"Path does not exist: {path}")
if not path.is_dir():
raise FileNotFoundError(f"Path is not a directory: {path}")
command = [
cls.path(),
"build",
str(path),
]
return subprocess.call(command)
class ResponderServer(threading.Thread):
"""
A threaded wrapper around the `responder run` command for testing purposes.
This class allows running a Responder application in a separate thread,
making it suitable for integration testing scenarios.
Args:
target (str): The path to the Responder application to run
port (int, optional): The port to run the server on. Defaults to 5042.
limit_max_requests (int, optional): Maximum number of requests to handle
before shutting down. Useful for testing scenarios.
Example:
>>> server = ResponderServer("app.py", port=8000)
>>> server.start()
>>> # Run tests
>>> server.stop()
"""
def __init__(self, target: str, port: int = 5042, limit_max_requests: int = None):
super().__init__()
self._stopping = False
# Validate input variables.
if not target or not isinstance(target, str):
raise ValueError("Target must be a non-empty string")
if not isinstance(port, int) or port < 1:
raise ValueError("Port must be a positive integer")
if limit_max_requests is not None and (
not isinstance(limit_max_requests, int) or limit_max_requests < 1
):
raise ValueError("limit_max_requests must be a positive integer if specified")
# Check if port is available.
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", port))
except OSError as ex:
raise ValueError(f"Port {port} is already in use") from ex
# Instance variables after validation.
self.target = target
self.port = port
self.limit_max_requests = limit_max_requests
self.shutdown_timeout = 5 # seconds
# Allow the thread to be terminated when the main program exits.
self.process: subprocess.Popen
self.daemon = True
self._process_lock = threading.Lock()
# Setup signal handlers.
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
def run(self):
command = [
ResponderProgram.path(),
"run",
self.target,
]
if self.limit_max_requests is not None:
command += [f"--limit-max-requests={self.limit_max_requests}"]
# Preserve existing environment
env = os.environ.copy()
if self.port is not None:
env["PORT"] = str(self.port)
with self._process_lock:
self.process = subprocess.Popen(
command,
env=env,
universal_newlines=True,
)
self.process.wait()
def stop(self):
"""
Gracefully stop the process (API).
"""
if self._stopping:
return
with self._process_lock:
self._stop()
def _stop(self):
"""
Gracefully stop the process (impl).
"""
self._stopping = True
if self.process and self.process.poll() is None:
logger.info("Attempting to terminate server process...")
self.process.terminate()
try:
# Wait for graceful shutdown.
self.process.wait(timeout=self.shutdown_timeout)
logger.info("Server process terminated gracefully")
except subprocess.TimeoutExpired:
logger.warning(
"Server process did not terminate gracefully, forcing kill"
)
self.process.kill() # Force kill if not terminated
def _signal_handler(self, signum, frame):
"""
Handle termination signals gracefully.
"""
logger.info("Received signal %d, shutting down...", signum)
self.stop()
def wait_until_ready(self, timeout=30, request_timeout=1, delay=0.1) -> bool:
"""
Wait until the server is ready to accept connections.
Args:
timeout (int, optional): Maximum time to wait in seconds. Defaults to 30.
Returns:
bool: True if server is ready and accepting connections, False otherwise.
"""
start_time = time.time()
last_error = None
while time.time() - start_time < timeout:
if not self.is_running():
if self.process is None:
logger.error("Server process was never started")
else:
returncode = self.process.poll()
logger.error("Server process exited with code: %d", returncode)
return False
try:
with socket.create_connection(
("localhost", self.port), timeout=request_timeout
):
return True
except (
socket.timeout,
ConnectionRefusedError,
socket.gaierror,
OSError,
) as ex:
last_error = ex
logger.debug(f"Server not ready yet: {ex}")
time.sleep(delay)
logger.error(
"Server failed to start within %d seconds. Last error: %s",
timeout,
last_error,
)
return False
def is_running(self):
"""
Check if the server process is still running.
"""
return self.process is not None and self.process.poll() is None
-44
View File
@@ -1,44 +0,0 @@
import logging
import typing as t
from pueblo.sfa.core import InvalidTarget, SingleFileApplication
__all__ = [
"InvalidTarget",
"SingleFileApplication",
"load_target",
]
logger = logging.getLogger(__name__)
def load_target(target: str, default_property: str = "api", method: str = "run") -> t.Any:
"""
Load Python code from a file path or module name.
Warning:
This function executes arbitrary Python code. Ensure the target is from a trusted
source to prevent security vulnerabilities.
Args:
target: Module address (e.g., 'acme.app:foo'), file path (e.g., '/path/to/acme/app.py'),
or URL.
default_property: Name of the property to load if not specified in target (default: "api")
method: Name of the method to invoke on the API instance (default: "run")
Returns:
The API instance, loaded from the given property.
Raises:
ValueError: If target format is invalid
ImportError: If module cannot be imported
AttributeError: If property or method is not found
Example:
>>> api = load_target("myapp.api:server")
>>> api.run()
""" # noqa: E501
app = SingleFileApplication.from_spec(spec=target, default_property=default_property)
app.load()
return app.entrypoint

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