mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30801557a3 | |||
| 73d46e9b03 | |||
| 3d65d88ea9 | |||
| 8f979719a0 | |||
| 0cbcaf9c4f | |||
| 3fa6f11ffa | |||
| 8b88b148bf | |||
| 1aecafa82a | |||
| 8c763aa97e | |||
| 91aa242a5a | |||
| 084d057a99 | |||
| d3acf2c1c1 | |||
| 80715a12ac | |||
| 66fc7afbe4 | |||
| e7776eb9e8 | |||
| 944d47da45 | |||
| a3a12cff77 | |||
| 7b2839086d | |||
| 351ff8d95e | |||
| 2278beba18 | |||
| 3cfc7ec2b6 | |||
| 0de22eeed2 | |||
| b0cc37861b | |||
| 7d4532acc9 | |||
| 1b63d2943a | |||
| b5723303c8 | |||
| 5730be4b31 | |||
| 6f9c11645a | |||
| 827cc64988 | |||
| 7b5db5bc33 | |||
| b9a03c7088 | |||
| 4cbf55508e | |||
| 83d0fcf1ae | |||
| a698eaaab3 | |||
| 3aa21eed08 | |||
| 2741c74b90 | |||
| aba96525ad | |||
| a5b6d36991 | |||
| e4cff76fa6 | |||
| f11ad7136d | |||
| c32e8c7468 | |||
| d93e3cd12c | |||
| 040f1a57e4 | |||
| 307313744f | |||
| 98ca45003b | |||
| ab76594297 | |||
| 7fba0f6362 | |||
| 4ff73e9d0c | |||
| 68bbea0a55 | |||
| 106e5e9073 | |||
| 3426aa71da | |||
| 413028b636 | |||
| 3edf979a8c | |||
| cd75deeb4e | |||
| b71bb5ddb9 | |||
| 27a9459f22 | |||
| b39c539d57 | |||
| 718b53cce2 | |||
| 2e0b4975f7 | |||
| a118a5dc4b | |||
| 69c1d7f185 | |||
| fba2f135a3 | |||
| 4006de72cd | |||
| b3c7252197 | |||
| 398ac3343e | |||
| 8b197ba361 | |||
| e700aa2937 | |||
| 3894550642 | |||
| 43fd041138 | |||
| 363af5338d | |||
| 55430a4366 | |||
| f7c6a3ae97 | |||
| dcadba1425 | |||
| de08b15ae8 | |||
| 0cfca6d906 | |||
| a73e413a66 | |||
| 87931a25d0 | |||
| 1fd9a682dd | |||
| 5d3e650901 | |||
| 48d082e6a5 | |||
| 87e22481e8 | |||
| e48ce6c301 | |||
| e9613500da | |||
| c2943accd0 | |||
| 649a255657 | |||
| 7eaaaaafe1 | |||
| ae09b88978 | |||
| e3e307fd68 | |||
| 89f0724029 | |||
| bebe62adaf | |||
| eb9cddc8c2 | |||
| 7c19eca78a | |||
| ed28b11d21 | |||
| 46cdd4a245 | |||
| ac91b172e6 | |||
| ed0da6d462 | |||
| 555e9bff65 | |||
| bf43d9f202 | |||
| e239cc304d | |||
| 3285bd57c7 | |||
| 3090fb9e68 | |||
| e90bd24ebe | |||
| a0acc03a97 | |||
| 8a668e6efe | |||
| 4c75742e4d | |||
| 796fdc2ddf | |||
| a8caa3054b | |||
| 2ef9e133ad | |||
| 2ec570ad61 | |||
| 02aa338970 | |||
| 882250bd86 | |||
| 3809eda2f2 | |||
| b32eda70d2 | |||
| f1b2f46a10 | |||
| cf82dac4ad | |||
| a0913e3f63 | |||
| f90955a9b9 | |||
| 3736c9229d | |||
| a802853367 | |||
| 96ca88fe88 | |||
| a57570210a | |||
| 7682e94b35 | |||
| 8bbebe113c | |||
| 7c921f827b | |||
| 4cc055f93a | |||
| e596a8b457 | |||
| fd2da55880 | |||
| 975e9b5643 | |||
| c0036e0474 | |||
| 103816e27a | |||
| b7c1684ab4 | |||
| 16bd6ca266 | |||
| 20bae4712b | |||
| a7aa80c690 | |||
| df89d1d58b | |||
| 477cddd29c | |||
| 9b8cf3a1b1 | |||
| 2871a3c07f | |||
| 13763296dd | |||
| 783b22ab1c | |||
| 109937adf4 | |||
| 63ea9cc4e0 | |||
| ec40a0c4c3 | |||
| 0855d1a378 | |||
| 77fe17d350 | |||
| 0b8a031ccb |
@@ -0,0 +1,3 @@
|
|||||||
|
github: kennethreitz
|
||||||
|
thanks_dev: kennethreitz
|
||||||
|
custom: https://cash.app/$KennethReitz
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: "Tests"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request: ~
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Cancel redundant in-progress jobs.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: "Python ${{ matrix.python-version }}"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: [
|
||||||
|
"3.9",
|
||||||
|
"3.10",
|
||||||
|
"3.11",
|
||||||
|
"3.12",
|
||||||
|
"3.13",
|
||||||
|
]
|
||||||
|
env:
|
||||||
|
UV_SYSTEM_PYTHON: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Set up uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
enable-cache: true
|
||||||
|
cache-suffix: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-glob: |
|
||||||
|
pyproject.toml
|
||||||
|
|
||||||
|
- name: Install package
|
||||||
|
run: uv pip install '.[develop,test]'
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
.venv*
|
||||||
.vscode/
|
.vscode/
|
||||||
.cache
|
.cache
|
||||||
.idea
|
.idea
|
||||||
@@ -6,6 +7,7 @@
|
|||||||
.pytest_cache
|
.pytest_cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
.coverage*
|
||||||
|
|
||||||
__pycache__
|
__pycache__
|
||||||
tests/__pycache__
|
tests/__pycache__
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# .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
@@ -1,18 +0,0 @@
|
|||||||
# 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
|
|
||||||
+196
-48
@@ -1,274 +1,422 @@
|
|||||||
# 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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
## [v2.0.4] - 2019-11-19
|
|
||||||
|
## [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
|
### Fixed
|
||||||
|
|
||||||
|
- Routing: Fixed dispatching `static_route=None` on Windows
|
||||||
|
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
|
||||||
|
- Tests: Fixed deprecated httpx TestClient usage
|
||||||
|
|
||||||
|
## [v2.0.5] - 2019-12-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Update requirements to support python 3.8
|
||||||
|
|
||||||
|
## [v2.0.4] - 2019-11-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
- Fix static app resolving
|
- 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/taoufik07/responder/compare/v2.0.4..HEAD
|
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.0.0..HEAD
|
||||||
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
|
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
|
||||||
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
|
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
|
||||||
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
|
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
|
||||||
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
|
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
|
||||||
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
|
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
|
||||||
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
|
||||||
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
|
||||||
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
|
||||||
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
|
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
|
||||||
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
|
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
|
||||||
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
|
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
|
||||||
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
|
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
|
||||||
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
|
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
|
||||||
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
|
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
|
||||||
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
|
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
|
||||||
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
|
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
|
||||||
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
|
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
|
||||||
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
|
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
|
||||||
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
|
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
|
||||||
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
|
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
|
||||||
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
|
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
|
||||||
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
|
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
|
||||||
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
|
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
|
||||||
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
|
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
|
||||||
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
|
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
|
||||||
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
|
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
|
||||||
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
|
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
|
||||||
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
|
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
|
||||||
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
|
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
|
||||||
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
|
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
|
||||||
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
|
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
|
||||||
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
|
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
|
||||||
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
|
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
|
||||||
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
|
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
|
||||||
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
|
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
|
||||||
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
|
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
|
||||||
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
|
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
|
||||||
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
|
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
|
||||||
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
|
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
|
||||||
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
|
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
|
||||||
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
|
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
|
||||||
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
|
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
|
||||||
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
|
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
|
||||||
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
|
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
|
||||||
|
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
|
||||||
|
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
|
||||||
|
|||||||
@@ -1,13 +1,178 @@
|
|||||||
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
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
1. Definitions.
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
include LICENSE
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
[[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
@@ -1,762 +0,0 @@
|
|||||||
{
|
|
||||||
"_meta": {
|
|
||||||
"hash": {
|
|
||||||
"sha256": "ea12c0d556a3ca0848b0eba291a11a5ea98a701f0885c2d030b2aeb1e5b9c15f"
|
|
||||||
},
|
|
||||||
"pipfile-spec": 6,
|
|
||||||
"requires": {},
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"name": "pypi",
|
|
||||||
"url": "https://pypi.org/simple",
|
|
||||||
"verify_ssl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"default": {
|
|
||||||
"aiofiles": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
|
||||||
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
|
||||||
],
|
|
||||||
"version": "==0.4.0"
|
|
||||||
},
|
|
||||||
"aniso8601": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e",
|
|
||||||
"sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"
|
|
||||||
],
|
|
||||||
"version": "==7.0.0"
|
|
||||||
},
|
|
||||||
"apispec": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5fdaa1173b32515cc83f9d413a49a6c37fafc2b87f6b40e95923d3e85f0942c5",
|
|
||||||
"sha256:9e88c51517a6515612e818459f61c1bc06c00f2313e5187828bdbabaa7461473"
|
|
||||||
],
|
|
||||||
"version": "==3.0.0"
|
|
||||||
},
|
|
||||||
"apistar": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:8da0d3f15748c8ed6e68914ba5b8f6dd5dff5afbe137950d07103575df0bce73"
|
|
||||||
],
|
|
||||||
"version": "==0.7.2"
|
|
||||||
},
|
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
|
||||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
|
||||||
],
|
|
||||||
"version": "==2019.9.11"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
|
||||||
],
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"click": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
|
||||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"docopt": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
|
||||||
],
|
|
||||||
"version": "==0.6.2"
|
|
||||||
},
|
|
||||||
"graphene": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896",
|
|
||||||
"sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"
|
|
||||||
],
|
|
||||||
"version": "==2.1.8"
|
|
||||||
},
|
|
||||||
"graphql-core": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1488f2a5c2272dc9ba66e3042a6d1c30cea0db4c80bd1e911c6791ad6187d91b",
|
|
||||||
"sha256:da64c472d720da4537a2e8de8ba859210b62841bd47a9be65ca35177f62fe0e4"
|
|
||||||
],
|
|
||||||
"version": "==2.2.1"
|
|
||||||
},
|
|
||||||
"graphql-relay": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335",
|
|
||||||
"sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5",
|
|
||||||
"sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6"
|
|
||||||
],
|
|
||||||
"version": "==2.0.0"
|
|
||||||
},
|
|
||||||
"graphql-server-core": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
|
|
||||||
],
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
|
||||||
"h11": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
|
||||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
|
||||||
],
|
|
||||||
"version": "==0.8.1"
|
|
||||||
},
|
|
||||||
"httptools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
|
|
||||||
],
|
|
||||||
"version": "==0.0.13"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
|
||||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
|
||||||
],
|
|
||||||
"version": "==2.8"
|
|
||||||
},
|
|
||||||
"itsdangerous": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
|
||||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"jinja2": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
|
||||||
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
|
||||||
],
|
|
||||||
"version": "==2.10.3"
|
|
||||||
},
|
|
||||||
"markupsafe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
|
||||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
|
||||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
|
||||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
|
||||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
|
||||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
|
||||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
|
||||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
|
||||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
|
||||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
|
||||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
|
||||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
|
||||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
|
||||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
|
||||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
|
||||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
|
||||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
|
||||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
|
||||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
|
||||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
|
||||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
|
||||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
|
||||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
|
||||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
|
||||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
|
||||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
|
||||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
|
||||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
|
||||||
],
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
|
||||||
"marshmallow": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
|
|
||||||
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
|
|
||||||
],
|
|
||||||
"version": "==3.2.1"
|
|
||||||
},
|
|
||||||
"promise": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
|
|
||||||
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
|
|
||||||
],
|
|
||||||
"version": "==2.2.1"
|
|
||||||
},
|
|
||||||
"python-multipart": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
|
||||||
],
|
|
||||||
"version": "==0.0.5"
|
|
||||||
},
|
|
||||||
"pyyaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
|
||||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
|
||||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
|
||||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
|
||||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
|
||||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
|
||||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
|
||||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
|
||||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
|
||||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
|
||||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
|
||||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
|
||||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
|
||||||
],
|
|
||||||
"version": "==5.1.2"
|
|
||||||
},
|
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
|
||||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
|
||||||
],
|
|
||||||
"version": "==2.22.0"
|
|
||||||
},
|
|
||||||
"requests-toolbelt": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
|
||||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
|
||||||
],
|
|
||||||
"version": "==0.9.1"
|
|
||||||
},
|
|
||||||
"responder": {
|
|
||||||
"editable": true,
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
"rfc3986": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405",
|
|
||||||
"sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"
|
|
||||||
],
|
|
||||||
"version": "==1.3.2"
|
|
||||||
},
|
|
||||||
"rx": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
|
|
||||||
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
|
|
||||||
],
|
|
||||||
"version": "==1.6.1"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
|
||||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
|
||||||
],
|
|
||||||
"version": "==1.12.0"
|
|
||||||
},
|
|
||||||
"starlette": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e41ef52e711a82ef95c195674e5d8d41c75c6b1d6f5a275637eedd4cc2150a7f"
|
|
||||||
],
|
|
||||||
"version": "==0.12.10"
|
|
||||||
},
|
|
||||||
"typesystem": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ba2bd10f1c5844d08dd8841e777bdee55bfca569bf21cb96cd0f91e0a4f66cd8"
|
|
||||||
],
|
|
||||||
"version": "==0.2.4"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
|
||||||
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
|
||||||
],
|
|
||||||
"version": "==1.25.6"
|
|
||||||
},
|
|
||||||
"uvicorn": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:8aa44f9d9c3082ef693950387ea25d376e32944df6d4071dbd8edc3c25a40c74"
|
|
||||||
],
|
|
||||||
"version": "==0.8.6"
|
|
||||||
},
|
|
||||||
"uvloop": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
|
|
||||||
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
|
|
||||||
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
|
|
||||||
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
|
|
||||||
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
|
|
||||||
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
|
|
||||||
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
|
|
||||||
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
|
|
||||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
|
||||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
|
||||||
],
|
|
||||||
"version": "==0.12.2"
|
|
||||||
},
|
|
||||||
"websockets": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
|
||||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
|
||||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
|
||||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
|
||||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
|
||||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
|
||||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
|
||||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
|
||||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
|
||||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
|
||||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
|
||||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
|
||||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
|
||||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
|
||||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
|
||||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
|
||||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
|
||||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
|
||||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
|
||||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
|
||||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"whitenoise": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:22f79cf8f1f509639330f93886acaece8ec5ac5e9600c3b981d33c34e8a42dfd",
|
|
||||||
"sha256:6dfea214b7c12efd689007abf9afa87a426586e9dbc051873ad2c8e535e2a1ac"
|
|
||||||
],
|
|
||||||
"version": "==4.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"develop": {
|
|
||||||
"alabaster": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
|
||||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
|
||||||
],
|
|
||||||
"version": "==0.7.12"
|
|
||||||
},
|
|
||||||
"appdirs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
|
||||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
|
||||||
],
|
|
||||||
"version": "==1.4.3"
|
|
||||||
},
|
|
||||||
"atomicwrites": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
|
||||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
|
||||||
],
|
|
||||||
"version": "==1.3.0"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
|
||||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
|
||||||
],
|
|
||||||
"version": "==19.3.0"
|
|
||||||
},
|
|
||||||
"babel": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
|
|
||||||
"sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
|
|
||||||
],
|
|
||||||
"version": "==2.7.0"
|
|
||||||
},
|
|
||||||
"black": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
|
|
||||||
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==19.3b0"
|
|
||||||
},
|
|
||||||
"bleach": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
|
||||||
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
|
||||||
],
|
|
||||||
"version": "==3.1.0"
|
|
||||||
},
|
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
|
||||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
|
||||||
],
|
|
||||||
"version": "==2019.9.11"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
|
||||||
],
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"click": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
|
||||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"coverage": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:17a417c691de3fc88de027832267313e5ed2b2ea3956745b562c4c389e44d05b",
|
|
||||||
"sha256:24307e67ebd9dc06fcbab9b7fef87412a97746c1baabb04ed8a93d5c2ccfe5ba",
|
|
||||||
"sha256:2a5d44a9d8426bd3699123864e63f008dc8dea9df22d5216a141a25d4670f22c",
|
|
||||||
"sha256:3726b8f5461e103a40e380f52b4b4ccdf2eda55d5d72f037cee43627992b4462",
|
|
||||||
"sha256:39dd15bbc4880a64399e180925bbc21c0c316a3065f6455d2512039f5cb59b94",
|
|
||||||
"sha256:3bb121f5dd156aab4fba2ebad6b0ad605bc5dc305931140dc614b101aa9d81ed",
|
|
||||||
"sha256:3bfdea9226eaed97736c973a7d6d0bbf9e1c1f1c7391c8e9c2bb2d0dbae49156",
|
|
||||||
"sha256:43be906a16239c1aa9f3742e3e6b0a5dd24781a13ce401f063262e9b4e93b69f",
|
|
||||||
"sha256:4a54cac1b39b2925041a41bcd1f191898fe401618627d7c3abf127c32a1c6dd1",
|
|
||||||
"sha256:4e58d65b90d6f26b3ccca7cf0fe573ef847347b8734af596a087a21eebb681f5",
|
|
||||||
"sha256:50229727d9baf0cd7f5ee6b194bf9dea708e9a20823d93f9e04d710b0a60e757",
|
|
||||||
"sha256:5141cdb010e9cd6939e37b8c2769d535cb535d80ef94f927c8a306f2e05a4736",
|
|
||||||
"sha256:748ba2b950425b9aef9d1bde2d6af7023585505016bd634e578f76ada4a30465",
|
|
||||||
"sha256:75e635bc6730c88b04421b25a0afc47b9b80efc1ed57630839196eb475722e50",
|
|
||||||
"sha256:78556f51dbfb33f18794eee29a4a8542fd2e301aa0d072653930793974dced03",
|
|
||||||
"sha256:7de17133509210ecc256535bab2f9a5547f3016c44f984fe12b4c10d81a4623f",
|
|
||||||
"sha256:83bf376555898fe2dc50d111a34b0152b504e454ed1e13cdcda6e5d50ba0ed5b",
|
|
||||||
"sha256:87730b5e4c3a42674fe8f0ecbb0d556c59c7e12b11a65c2178f2787252a80dfd",
|
|
||||||
"sha256:9bb7819c020c20c6200764879f0b10b323d6d4719aa7b0ae316c9e35730f9e2d",
|
|
||||||
"sha256:9c825788acb13d49ac20455433f3b862029aa497e97faba8c998555a042a6b91",
|
|
||||||
"sha256:b2bb4941c8838fc9ea2fca3c52e6dd865d39bbbc014bde249161bf8fcccf2152",
|
|
||||||
"sha256:c1b44c6c680f137910cb0f5481a2ae9899787ca7019f110a3708d9e99df941be",
|
|
||||||
"sha256:c52c2bc67bd3ff8db685f7c5f03e34a95bddd58a535630161f28d1c485d61e22",
|
|
||||||
"sha256:d6845e46338695c571759be1c770b013c477111e785b26151ec9feb6cd063543",
|
|
||||||
"sha256:e292b32dfc80d9f271af2d52df95455248322156e764763c4bfb2385b2e33533"
|
|
||||||
],
|
|
||||||
"version": "==5.0a8"
|
|
||||||
},
|
|
||||||
"docutils": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
|
|
||||||
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
|
|
||||||
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
|
|
||||||
],
|
|
||||||
"version": "==0.15.2"
|
|
||||||
},
|
|
||||||
"entrypoints": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
|
||||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
|
||||||
],
|
|
||||||
"version": "==0.3"
|
|
||||||
},
|
|
||||||
"flake8": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
|
|
||||||
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.7.8"
|
|
||||||
},
|
|
||||||
"flask": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
|
||||||
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
|
||||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
|
||||||
],
|
|
||||||
"version": "==2.8"
|
|
||||||
},
|
|
||||||
"imagesize": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
|
||||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"importlib-metadata": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
|
|
||||||
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
|
||||||
],
|
|
||||||
"markers": "python_version < '3.8'",
|
|
||||||
"version": "==0.23"
|
|
||||||
},
|
|
||||||
"itsdangerous": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
|
||||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"jinja2": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
|
||||||
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
|
||||||
],
|
|
||||||
"version": "==2.10.3"
|
|
||||||
},
|
|
||||||
"markupsafe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
|
||||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
|
||||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
|
||||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
|
||||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
|
||||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
|
||||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
|
||||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
|
||||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
|
||||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
|
||||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
|
||||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
|
||||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
|
||||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
|
||||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
|
||||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
|
||||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
|
||||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
|
||||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
|
||||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
|
||||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
|
||||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
|
||||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
|
||||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
|
||||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
|
||||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
|
||||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
|
||||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
|
||||||
],
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
|
||||||
"marshmallow": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83",
|
|
||||||
"sha256:9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"
|
|
||||||
],
|
|
||||||
"version": "==3.2.1"
|
|
||||||
},
|
|
||||||
"mccabe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
|
||||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
|
||||||
],
|
|
||||||
"version": "==0.6.1"
|
|
||||||
},
|
|
||||||
"more-itertools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
|
||||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
|
||||||
],
|
|
||||||
"version": "==7.2.0"
|
|
||||||
},
|
|
||||||
"packaging": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
|
|
||||||
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
|
|
||||||
],
|
|
||||||
"version": "==19.2"
|
|
||||||
},
|
|
||||||
"pkginfo": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
|
|
||||||
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
|
|
||||||
],
|
|
||||||
"version": "==1.5.0.1"
|
|
||||||
},
|
|
||||||
"pluggy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
|
|
||||||
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
|
|
||||||
],
|
|
||||||
"version": "==0.13.0"
|
|
||||||
},
|
|
||||||
"py": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
|
|
||||||
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
|
|
||||||
],
|
|
||||||
"version": "==1.8.0"
|
|
||||||
},
|
|
||||||
"pycodestyle": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
|
||||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
|
||||||
],
|
|
||||||
"version": "==2.5.0"
|
|
||||||
},
|
|
||||||
"pyflakes": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
|
||||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
|
||||||
],
|
|
||||||
"version": "==2.1.1"
|
|
||||||
},
|
|
||||||
"pygments": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
|
|
||||||
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
|
|
||||||
],
|
|
||||||
"version": "==2.4.2"
|
|
||||||
},
|
|
||||||
"pyparsing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
|
|
||||||
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
|
|
||||||
],
|
|
||||||
"version": "==2.4.2"
|
|
||||||
},
|
|
||||||
"pytest": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
|
|
||||||
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==5.2.1"
|
|
||||||
},
|
|
||||||
"pytest-cov": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
|
|
||||||
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.8.1"
|
|
||||||
},
|
|
||||||
"pytz": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
|
||||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
|
||||||
],
|
|
||||||
"version": "==2019.3"
|
|
||||||
},
|
|
||||||
"readme-renderer": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
|
|
||||||
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
|
|
||||||
],
|
|
||||||
"version": "==24.0"
|
|
||||||
},
|
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
|
||||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
|
||||||
],
|
|
||||||
"version": "==2.22.0"
|
|
||||||
},
|
|
||||||
"requests-toolbelt": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
|
||||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
|
||||||
],
|
|
||||||
"version": "==0.9.1"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
|
||||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
|
||||||
],
|
|
||||||
"version": "==1.12.0"
|
|
||||||
},
|
|
||||||
"snowballstemmer": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
|
|
||||||
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
|
|
||||||
],
|
|
||||||
"version": "==2.0.0"
|
|
||||||
},
|
|
||||||
"sphinx": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845",
|
|
||||||
"sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.2.0"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-applehelp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
|
|
||||||
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
|
|
||||||
],
|
|
||||||
"version": "==1.0.1"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-devhelp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
|
|
||||||
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
|
|
||||||
],
|
|
||||||
"version": "==1.0.1"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-htmlhelp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
|
|
||||||
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
|
|
||||||
],
|
|
||||||
"version": "==1.0.2"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-jsmath": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
|
||||||
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
|
||||||
],
|
|
||||||
"version": "==1.0.1"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-qthelp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
|
|
||||||
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
|
|
||||||
],
|
|
||||||
"version": "==1.0.2"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-serializinghtml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
|
|
||||||
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
|
|
||||||
],
|
|
||||||
"version": "==1.1.3"
|
|
||||||
},
|
|
||||||
"toml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
|
||||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
|
||||||
],
|
|
||||||
"version": "==0.10.0"
|
|
||||||
},
|
|
||||||
"tqdm": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:abc25d0ce2397d070ef07d8c7e706aede7920da163c64997585d42d3537ece3d",
|
|
||||||
"sha256:dd3fcca8488bb1d416aa7469d2f277902f26260c45aa86b667b074cd44b3b115"
|
|
||||||
],
|
|
||||||
"version": "==4.36.1"
|
|
||||||
},
|
|
||||||
"twine": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1",
|
|
||||||
"sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.0.0"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
|
||||||
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
|
||||||
],
|
|
||||||
"version": "==1.25.6"
|
|
||||||
},
|
|
||||||
"wcwidth": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
|
||||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
|
||||||
],
|
|
||||||
"version": "==0.1.7"
|
|
||||||
},
|
|
||||||
"webencodings": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
|
||||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
|
||||||
],
|
|
||||||
"version": "==0.5.1"
|
|
||||||
},
|
|
||||||
"werkzeug": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
|
|
||||||
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
|
|
||||||
],
|
|
||||||
"version": "==0.16.0"
|
|
||||||
},
|
|
||||||
"zipp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
|
||||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
|
||||||
],
|
|
||||||
"version": "==0.6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,109 @@
|
|||||||
# Responder: a familiar HTTP Service Framework for Python
|
# Responder
|
||||||
|
|
||||||
[](https://travis-ci.org/taoufik07/responder)
|
A familiar HTTP Service Framework for Python, powered by [Starlette](https://www.starlette.io/).
|
||||||
[](https://responder.readthedocs.io/en/latest/)
|
|
||||||
[](https://pypi.org/project/responder/)
|
|
||||||
[](https://pypi.org/project/responder/)
|
|
||||||
[](https://pypi.org/project/responder/)
|
|
||||||
[](https://github.com/taoufik07/responder/graphs/contributors)
|
|
||||||
|
|
||||||
[](https://responder.readthedocs.io)
|
```python
|
||||||
|
import responder
|
||||||
|
|
||||||
|
api = responder.API()
|
||||||
|
|
||||||
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
|
@api.route("/{greeting}")
|
||||||
|
async def greet_world(req, resp, *, greeting):
|
||||||
|
resp.text = f"{greeting}, world!"
|
||||||
|
|
||||||
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.
|
if __name__ == "__main__":
|
||||||
|
api.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
$ pip install responder
|
||||||
|
|
||||||
## Testimonials
|
That's it. Supports Python 3.9+.
|
||||||
|
|
||||||
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
|
## The Basics
|
||||||
|
|
||||||
> "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.text` sends back text. `resp.html` sends back HTML. `resp.content` sends back bytes.
|
||||||
|
- `resp.media` sends back JSON (or YAML, with content negotiation).
|
||||||
|
- `resp.file("path.pdf")` serves a file with automatic content-type detection.
|
||||||
|
- `req.headers` is case-insensitive. `req.params` gives you query parameters.
|
||||||
|
- Both sync and async views work — the `async` is optional.
|
||||||
|
|
||||||
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
|
## Highlights
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Type-safe route parameters
|
||||||
|
@api.route("/users/{user_id:int}")
|
||||||
|
async def get_user(req, resp, *, user_id):
|
||||||
|
resp.media = {"id": user_id}
|
||||||
|
|
||||||
## More Examples
|
# HTTP method filtering
|
||||||
|
@api.route("/items", methods=["POST"])
|
||||||
|
async def create_item(req, resp):
|
||||||
|
data = await req.media()
|
||||||
|
resp.media = {"created": data}
|
||||||
|
|
||||||
See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
|
# Class-based views
|
||||||
|
@api.route("/things/{id}")
|
||||||
|
class ThingResource:
|
||||||
|
def on_get(self, req, resp, *, id):
|
||||||
|
resp.media = {"id": id}
|
||||||
|
def on_post(self, req, resp, *, id):
|
||||||
|
resp.text = "created"
|
||||||
|
|
||||||
|
# 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"}
|
||||||
|
|
||||||
# Installing Responder
|
# Custom error handling
|
||||||
|
@api.exception_handler(ValueError)
|
||||||
|
async def handle_error(req, resp, exc):
|
||||||
|
resp.status_code = 400
|
||||||
|
resp.media = {"error": str(exc)}
|
||||||
|
|
||||||
Install the stable release:
|
# Lifespan events
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app):
|
||||||
|
print("starting up")
|
||||||
|
yield
|
||||||
|
print("shutting down")
|
||||||
|
|
||||||
$ pipenv install responder
|
api = responder.API(lifespan=lifespan)
|
||||||
✨🍰✨
|
|
||||||
|
|
||||||
|
# GraphQL
|
||||||
|
import graphene
|
||||||
|
api.graphql("/graphql", schema=graphene.Schema(query=Query))
|
||||||
|
|
||||||
Or, install from the development branch:
|
# WebSockets
|
||||||
|
@api.route("/ws", websocket=True)
|
||||||
|
async def websocket(ws):
|
||||||
|
await ws.accept()
|
||||||
|
while True:
|
||||||
|
name = await ws.receive_text()
|
||||||
|
await ws.send_text(f"Hello {name}!")
|
||||||
|
|
||||||
$ pipenv install -e git+https://github.com/taoufik07/responder.git#egg=responder
|
# Mount WSGI/ASGI apps
|
||||||
|
from flask import Flask
|
||||||
|
flask_app = Flask(__name__)
|
||||||
|
api.mount("/flask", flask_app)
|
||||||
|
|
||||||
Only **Python 3.6+** is supported.
|
# Background tasks
|
||||||
|
@api.route("/work")
|
||||||
|
def do_work(req, resp):
|
||||||
|
@api.background.task
|
||||||
|
def process():
|
||||||
|
import time; time.sleep(10)
|
||||||
|
process()
|
||||||
|
resp.media = {"status": "processing"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
|
||||||
|
|
||||||
# The Basic Idea
|
Route convertors: `str`, `int`, `float`, `uuid`, `path`.
|
||||||
|
|
||||||
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.
|
## Documentation
|
||||||
|
|
||||||
- Setting `resp.content` sends back bytes.
|
https://responder.kennethreitz.org
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -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
+142
-132
@@ -10,142 +10,152 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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)
|
if (obj.addEventListener) obj.addEventListener(type, fn, false);
|
||||||
obj.addEventListener(type, fn, false);
|
else if (obj.attachEvent) {
|
||||||
else if (obj.attachEvent) {
|
// IE
|
||||||
// IE
|
obj["e" + type + fn] = fn;
|
||||||
obj["e" + type + fn] = fn;
|
obj[type + fn] = function () {
|
||||||
obj[type + fn] = function () {
|
obj["e" + type + fn](window.event, ref_obj);
|
||||||
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) {
|
||||||
removeEvent: function (obj, eventName, eventCallback) {
|
if (obj.removeEventListener) {
|
||||||
if (obj.removeEventListener) {
|
obj.removeEventListener(eventName, eventCallback);
|
||||||
obj.removeEventListener(eventName, eventCallback);
|
} else if (obj.attachEvent) {
|
||||||
} else if (obj.attachEvent) {
|
obj.detachEvent(eventName);
|
||||||
obj.detachEvent(eventName);
|
}
|
||||||
}
|
},
|
||||||
},
|
input: "",
|
||||||
input: "",
|
pattern: "38384040373937396665",
|
||||||
pattern: "38384040373937396665",
|
keydownHandler: function (e, ref_obj) {
|
||||||
keydownHandler: function (e, ref_obj) {
|
if (ref_obj) {
|
||||||
if (ref_obj) {
|
konami = ref_obj;
|
||||||
konami = ref_obj;
|
} // IE
|
||||||
} // IE
|
konami.input += e ? e.keyCode : event.keyCode;
|
||||||
konami.input += e ? e.keyCode : event.keyCode;
|
if (konami.input.length > konami.pattern.length) {
|
||||||
if (konami.input.length > konami.pattern.length) {
|
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
|
||||||
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
|
}
|
||||||
}
|
if (konami.input === konami.pattern) {
|
||||||
if (konami.input === konami.pattern) {
|
konami.code(konami._currentLink);
|
||||||
konami.code(konami._currentLink);
|
konami.input = "";
|
||||||
konami.input = '';
|
e.preventDefault();
|
||||||
e.preventDefault();
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
},
|
||||||
},
|
load: function (link) {
|
||||||
load: function (link) {
|
this._currentLink = link;
|
||||||
this._currentLink = link;
|
this.addEvent(document, "keydown", this.keydownHandler, this);
|
||||||
this.addEvent(document, "keydown", this.keydownHandler, this);
|
this.iphone.load(link);
|
||||||
this.iphone.load(link);
|
},
|
||||||
},
|
unload: function () {
|
||||||
unload: function () {
|
this.removeEvent(document, "keydown", this.keydownHandler);
|
||||||
this.removeEvent(document, 'keydown', this.keydownHandler);
|
this.iphone.unload();
|
||||||
this.iphone.unload();
|
},
|
||||||
},
|
code: function (link) {
|
||||||
code: function (link) {
|
window.location = link;
|
||||||
window.location = link
|
},
|
||||||
},
|
iphone: {
|
||||||
iphone: {
|
start_x: 0,
|
||||||
start_x: 0,
|
start_y: 0,
|
||||||
start_y: 0,
|
stop_x: 0,
|
||||||
stop_x: 0,
|
stop_y: 0,
|
||||||
stop_y: 0,
|
tap: false,
|
||||||
tap: false,
|
capture: false,
|
||||||
capture: false,
|
orig_keys: "",
|
||||||
orig_keys: "",
|
keys: [
|
||||||
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
|
"UP",
|
||||||
input: [],
|
"UP",
|
||||||
code: function (link) {
|
"DOWN",
|
||||||
konami.code(link);
|
"DOWN",
|
||||||
},
|
"LEFT",
|
||||||
touchmoveHandler: function (e) {
|
"RIGHT",
|
||||||
if (e.touches.length === 1 && konami.iphone.capture === true) {
|
"LEFT",
|
||||||
var touch = e.touches[0];
|
"RIGHT",
|
||||||
konami.iphone.stop_x = touch.pageX;
|
"TAP",
|
||||||
konami.iphone.stop_y = touch.pageY;
|
"TAP",
|
||||||
konami.iphone.tap = false;
|
],
|
||||||
konami.iphone.capture = false;
|
input: [],
|
||||||
konami.iphone.check_direction();
|
code: function (link) {
|
||||||
}
|
konami.code(link);
|
||||||
},
|
},
|
||||||
touchendHandler: function () {
|
touchmoveHandler: function (e) {
|
||||||
konami.iphone.input.push(konami.iphone.check_direction());
|
if (e.touches.length === 1 && konami.iphone.capture === true) {
|
||||||
|
var touch = e.touches[0];
|
||||||
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
|
konami.iphone.stop_x = touch.pageX;
|
||||||
|
konami.iphone.stop_y = touch.pageY;
|
||||||
if (konami.iphone.input.length === konami.iphone.keys.length) {
|
konami.iphone.tap = false;
|
||||||
var match = true;
|
konami.iphone.capture = false;
|
||||||
for (var i = 0; i < konami.iphone.keys.length; i++) {
|
konami.iphone.check_direction();
|
||||||
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
|
|
||||||
match = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
konami.iphone.code(konami._currentLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
touchstartHandler: function (e) {
|
|
||||||
konami.iphone.start_x = e.changedTouches[0].pageX;
|
|
||||||
konami.iphone.start_y = e.changedTouches[0].pageY;
|
|
||||||
konami.iphone.tap = true;
|
|
||||||
konami.iphone.capture = true;
|
|
||||||
},
|
|
||||||
load: function (link) {
|
|
||||||
this.orig_keys = this.keys;
|
|
||||||
konami.addEvent(document, "touchmove", this.touchmoveHandler);
|
|
||||||
konami.addEvent(document, "touchend", this.touchendHandler, false);
|
|
||||||
konami.addEvent(document, "touchstart", this.touchstartHandler);
|
|
||||||
},
|
|
||||||
unload: function () {
|
|
||||||
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
|
|
||||||
konami.removeEvent(document, 'touchend', this.touchendHandler);
|
|
||||||
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
|
|
||||||
},
|
|
||||||
check_direction: function () {
|
|
||||||
x_magnitude = Math.abs(this.start_x - this.stop_x);
|
|
||||||
y_magnitude = Math.abs(this.start_y - this.stop_y);
|
|
||||||
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
|
|
||||||
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
|
|
||||||
result = (x_magnitude > y_magnitude) ? x : y;
|
|
||||||
result = (this.tap === true) ? "TAP" : result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
touchendHandler: function () {
|
||||||
|
konami.iphone.input.push(konami.iphone.check_direction());
|
||||||
|
|
||||||
typeof callback === "string" && konami.load(callback);
|
if (konami.iphone.input.length > konami.iphone.keys.length)
|
||||||
if (typeof callback === "function") {
|
konami.iphone.input.shift();
|
||||||
konami.code = callback;
|
|
||||||
konami.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
return konami;
|
if (konami.iphone.input.length === konami.iphone.keys.length) {
|
||||||
|
var match = true;
|
||||||
|
for (var i = 0; i < konami.iphone.keys.length; i++) {
|
||||||
|
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
|
||||||
|
match = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
konami.iphone.code(konami._currentLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
touchstartHandler: function (e) {
|
||||||
|
konami.iphone.start_x = e.changedTouches[0].pageX;
|
||||||
|
konami.iphone.start_y = e.changedTouches[0].pageY;
|
||||||
|
konami.iphone.tap = true;
|
||||||
|
konami.iphone.capture = true;
|
||||||
|
},
|
||||||
|
load: function (link) {
|
||||||
|
this.orig_keys = this.keys;
|
||||||
|
konami.addEvent(document, "touchmove", this.touchmoveHandler);
|
||||||
|
konami.addEvent(document, "touchend", this.touchendHandler, false);
|
||||||
|
konami.addEvent(document, "touchstart", this.touchstartHandler);
|
||||||
|
},
|
||||||
|
unload: function () {
|
||||||
|
konami.removeEvent(document, "touchmove", this.touchmoveHandler);
|
||||||
|
konami.removeEvent(document, "touchend", this.touchendHandler);
|
||||||
|
konami.removeEvent(document, "touchstart", this.touchstartHandler);
|
||||||
|
},
|
||||||
|
check_direction: function () {
|
||||||
|
x_magnitude = Math.abs(this.start_x - this.stop_x);
|
||||||
|
y_magnitude = Math.abs(this.start_y - this.stop_y);
|
||||||
|
x = this.start_x - this.stop_x < 0 ? "RIGHT" : "LEFT";
|
||||||
|
y = this.start_y - this.stop_y < 0 ? "DOWN" : "UP";
|
||||||
|
result = x_magnitude > y_magnitude ? x : y;
|
||||||
|
result = this.tap === true ? "TAP" : result;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
typeof callback === "string" && konami.load(callback);
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
konami.code = callback;
|
||||||
|
konami.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
return konami;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
module.exports = Konami;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="https://cloud.typography.com/7584432/7586812/css/fonts.css" />
|
<link
|
||||||
<script type="text/javascript">$('#searchbox').hide(0);</script>
|
rel="stylesheet"
|
||||||
|
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.) -->
|
||||||
@@ -39,9 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.method {
|
.method {
|
||||||
|
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.si,
|
.si,
|
||||||
@@ -80,8 +86,6 @@
|
|||||||
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;
|
||||||
@@ -118,10 +122,12 @@
|
|||||||
<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() { dataLayer.push(arguments); }
|
function gtag() {
|
||||||
gtag('js', new Date());
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
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. -->
|
||||||
@@ -130,7 +136,10 @@
|
|||||||
|
|
||||||
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
|
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var easter_egg = new Konami('https://www.myfortunecookie.co.uk/fortunes/' + (Math.floor(Math.random() * 152) + 1));
|
var easter_egg = new Konami(
|
||||||
|
"https://www.myfortunecookie.co.uk/fortunes/" +
|
||||||
|
(Math.floor(Math.random() * 152) + 1)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -140,67 +149,94 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- GitHub Logo -->
|
<!-- GitHub Logo -->
|
||||||
<a href="https://github.com/kennethreitz/responder" class="github-corner" aria-label="View source on GitHub">
|
<a
|
||||||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
|
href="https://github.com/kennethreitz/responder"
|
||||||
aria-hidden="true">
|
class="github-corner"
|
||||||
|
aria-label="View source on GitHub"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
viewBox="0 0 250 250"
|
||||||
|
style="fill: #151513; color: #fff; position: absolute; top: 0; border: 0; right: 0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
<path
|
||||||
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></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"
|
||||||
<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"
|
||||||
fill="currentColor" class="octo-body"></path>
|
style="transform-origin: 130px 106px"
|
||||||
|
class="octo-arm"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||||
|
fill="currentColor"
|
||||||
|
class="octo-body"
|
||||||
|
></path>
|
||||||
</svg>
|
</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>(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>
|
<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(['showTab', 'classic_widget', {
|
UserVoice.push([
|
||||||
mode: 'feedback',
|
"showTab",
|
||||||
primary_color: '#fa8c28',
|
"classic_widget",
|
||||||
link_color: '#0a8cc6',
|
{
|
||||||
forum_id: 913660,
|
mode: "feedback",
|
||||||
tab_label: 'Got feedback?',
|
primary_color: "#fa8c28",
|
||||||
tab_color: '#00994f',
|
link_color: "#0a8cc6",
|
||||||
tab_position: 'bottom-left',
|
forum_id: 913660,
|
||||||
tab_inverted: true
|
tab_label: "Got feedback?",
|
||||||
}]);
|
tab_color: "#00994f",
|
||||||
|
tab_position: "bottom-left",
|
||||||
|
tab_inverted: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,54 +1,88 @@
|
|||||||
<p class="logo">
|
<p class="logo">
|
||||||
<a href="{{ pathto(master_doc) }}">
|
<a href="{{ pathto(master_doc) }}">
|
||||||
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
|
<img
|
||||||
|
class="logo"
|
||||||
|
src="{{ pathto('_static/responder.png', 1) }}"
|
||||||
|
title="https://kennethreitz.org/tattoos"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
<a class="github-button"
|
||||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
href="https://github.com/kennethreitz/responder"
|
||||||
|
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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
|
<link
|
||||||
|
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 type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
|
<script
|
||||||
|
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>
|
<p><strong>Responder</strong> is a web service framework, written for human beings.</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><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
|
<p>
|
||||||
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
<a class="github-button"
|
||||||
|
href="https://github.com/kennethreitz"
|
||||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
|
data-color-scheme="no-preference: light; light: light; dark: light;"
|
||||||
@kennethreitz</a>
|
data-size="medium"
|
||||||
<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>
|
data-show-count="true"
|
||||||
|
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>
|
||||||
|
|||||||
@@ -1,54 +1,93 @@
|
|||||||
<p class="logo">
|
<p class="logo">
|
||||||
<a href="{{ pathto(master_doc) }}">
|
<a href="{{ pathto(master_doc) }}">
|
||||||
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" title="https://kennethreitz.org/tattoos" />
|
<img
|
||||||
|
class="logo"
|
||||||
|
src="{{ pathto('_static/responder.png', 1) }}"
|
||||||
|
title="https://kennethreitz.org/tattoos"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
<iframe
|
||||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
|
||||||
|
allowtransparency="true"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="0"
|
||||||
|
width="200px"
|
||||||
|
height="35px"
|
||||||
|
></iframe>
|
||||||
</p>
|
</p>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
|
<link
|
||||||
|
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 type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
|
<script
|
||||||
|
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>
|
<p><strong>Responder</strong> is a web service framework, written for human beings.</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><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
|
<p>
|
||||||
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
<iframe
|
||||||
|
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
|
||||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
|
allowtransparency="true"
|
||||||
@kennethreitz</a>
|
frameborder="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>
|
scrolling="0"
|
||||||
|
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>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 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
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../CHANGELOG.md
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
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
|
||||||
+74
-4
@@ -20,7 +20,7 @@
|
|||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = "responder"
|
project = "responder"
|
||||||
copyright = "2018, A Kenneth Reitz project"
|
copyright = "2018-2026, A Kenneth Reitz project"
|
||||||
author = "Kenneth Reitz"
|
author = "Kenneth Reitz"
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
@@ -57,6 +57,11 @@ 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.
|
||||||
@@ -66,7 +71,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"
|
source_suffix = {".rst": "restructuredtext"}
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
@@ -76,7 +81,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 = None
|
language = "en"
|
||||||
|
|
||||||
# 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.
|
||||||
@@ -211,12 +216,77 @@ 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 = {"https://docs.python.org/": None}
|
intersphinx_mapping = {"python": ("https://docs.python.org/3", 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
|
||||||
|
|||||||
+18
-21
@@ -6,33 +6,30 @@ You can deploy Responder anywhere you can deploy a basic Python application.
|
|||||||
Docker Deployment
|
Docker Deployment
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
|
Assuming an existing ``api.py`` containing your Responder application.
|
||||||
|
|
||||||
``Dockerfile``::
|
``Dockerfile``::
|
||||||
|
|
||||||
FROM kennethreitz/pipenv
|
FROM python:3.13-slim
|
||||||
ENV PORT '80'
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . .
|
||||||
CMD python3 api.py
|
RUN pip install responder
|
||||||
|
ENV PORT=80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
CMD ["python", "api.py"]
|
||||||
|
|
||||||
That's it!
|
That's it!
|
||||||
|
|
||||||
Heroku Deployment
|
Cloud 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``::
|
||||||
|
|
||||||
@@ -47,12 +44,12 @@ Write out an ``api.py``::
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
api.run()
|
api.run()
|
||||||
|
|
||||||
Write out a ``Procfile``::
|
Deploy with your platform of choice. Responder will bind to ``0.0.0.0``
|
||||||
|
on the port specified by ``PORT`` automatically.
|
||||||
|
|
||||||
web: python api.py
|
Running with Uvicorn Directly
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
That's it! Next, we commit and push to Heroku::
|
For production deployments, you can also run your app directly with uvicorn::
|
||||||
|
|
||||||
$ git add -A
|
uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
|
||||||
$ git commit -m 'initial commit'
|
|
||||||
$ git push heroku master
|
|
||||||
|
|||||||
+66
-33
@@ -6,19 +6,23 @@
|
|||||||
A familiar HTTP Service Framework
|
A familiar HTTP Service Framework
|
||||||
=================================
|
=================================
|
||||||
|
|
||||||
|Build Status| |image1| |image2| |image3| |image4| |image5|
|
|ci-tests| |version| |license| |python-versions| |downloads| |contributors| |say-thanks|
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/kennethreitz/responder.svg?branch=master
|
.. |ci-tests| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
|
||||||
:target: https://travis-ci.org/kennethreitz/responder
|
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
|
||||||
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
|
.. |ci-docs| image:: https://github.com/kennethreitz/responder/actions/workflows/docs.yaml/badge.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/
|
||||||
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
|
.. |license| image:: https://img.shields.io/pypi/l/responder.svg
|
||||||
:target: https://pypi.org/project/responder/
|
:target: https://pypi.org/project/responder/
|
||||||
.. |image3| image:: https://img.shields.io/pypi/pyversions/responder.svg
|
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/responder.svg
|
||||||
:target: https://pypi.org/project/responder/
|
:target: https://pypi.org/project/responder/
|
||||||
.. |image4| image:: https://img.shields.io/github/contributors/kennethreitz/responder.svg
|
.. |downloads| image:: https://static.pepy.tech/badge/responder/month
|
||||||
|
: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
|
||||||
.. |image5| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
|
.. |say-thanks| 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
|
||||||
@@ -34,23 +38,24 @@ A familiar HTTP Service Framework
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
api.run()
|
api.run()
|
||||||
|
|
||||||
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
|
Responder is powered by `Starlette`_.
|
||||||
|
|
||||||
This gets you a ASGI app, with a production static files server
|
The example program demonstrates an `ASGI`_ application using `Responder`_,
|
||||||
(`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
|
including production-ready components like the `uvicorn`_ webserver, based
|
||||||
pre-installed, jinja2 templating (without additional imports), and a
|
on `uvloop`_, and the `Jinja`_ templating library pre-installed.
|
||||||
production webserver based on uvloop, serving up requests with
|
The ``async`` declaration within the example program is optional.
|
||||||
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 <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
|
- `ASGI`_, 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 <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
|
- `f-string syntax`_ 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!
|
||||||
@@ -61,30 +66,23 @@ Testimonials
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
“Pleasantly very taken with python-responder.
|
“Pleasantly very taken with python-responder.
|
||||||
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
|
`@kennethreitz`_ at his absolute best.”
|
||||||
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
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@@ -96,17 +94,38 @@ User Guides
|
|||||||
deployment
|
deployment
|
||||||
testing
|
testing
|
||||||
api
|
api
|
||||||
|
cli
|
||||||
|
|
||||||
|
|
||||||
Installing Responder
|
Installing Responder
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
Use ``uv`` for fast installation.
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
$ pipenv install responder
|
uv pip install --upgrade 'responder'
|
||||||
✨🍰✨
|
|
||||||
|
|
||||||
Only **Python 3.6+** is supported.
|
Or use standard pip where ``uv`` is not available.
|
||||||
|
|
||||||
|
.. 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
|
||||||
@@ -123,14 +142,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 -- all while using Python 3.6+'s new f-string syntax.
|
- Flask-style route expression, with new capabilities -- using Python's 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 that uses the actual Requests you know and love**.
|
- **A built in testing client** powered by Starlette's TestClient.
|
||||||
- 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 <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.
|
- `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.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|
||||||
@@ -140,3 +159,17 @@ 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/
|
||||||
|
|||||||
@@ -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`` and ``float``.
|
Supported types: ``str``, ``int``, ``float``, ``uuid``, and ``path``.
|
||||||
|
|
||||||
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 `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
|
Responder provides a built-in light `Jinja`_ wrapper ``templates.Templates``
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
@@ -158,20 +158,19 @@ 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):
|
||||||
f = open('./{}'.format(data['file']['filename']), 'w')
|
with open(f"./{data['file']['filename']}", 'wb') as f:
|
||||||
f.write(data['file']['content'].decode('utf-8'))
|
f.write(data['file']['content'])
|
||||||
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 send a file easily with requests::
|
You can test file uploads using the built-in test client::
|
||||||
|
|
||||||
import requests
|
files = {'file': ('hello.txt', b'hello, world!', 'text/plain')}
|
||||||
|
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)
|
|
||||||
|
|
||||||
print(r.text)
|
.. _Jinja: https://jinja.palletsprojects.com/en/stable/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
(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
|
||||||
|
```
|
||||||
+4
-37
@@ -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: **Requests**.
|
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient).
|
||||||
|
|
||||||
Here, we'll go over the basics of setting up a proper Python package and adding testing to it.
|
Here, we'll go over the basics of setting up and testing a Responder application.
|
||||||
|
|
||||||
The Basics
|
The Basics
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Your repository should look like this::
|
Your project should look like this::
|
||||||
|
|
||||||
Pipfile Pipfile.lock api.py test_api.py
|
api.py test_api.py
|
||||||
|
|
||||||
``$ cat api.py``::
|
``$ cat api.py``::
|
||||||
|
|
||||||
@@ -25,26 +25,6 @@ Your repository 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
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -66,16 +46,3 @@ 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.
|
|
||||||
|
|||||||
+91
-11
@@ -2,6 +2,21 @@ 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
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
@@ -15,6 +30,61 @@ 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
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@@ -34,10 +104,15 @@ 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"))
|
||||||
@@ -46,7 +121,7 @@ Serve a GraphQL API::
|
|||||||
return f"Hello {name}"
|
return f"Hello {name}"
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
view = responder.ext.GraphQLView(api=api, schema=schema)
|
view = GraphQLView(api=api, schema=schema)
|
||||||
|
|
||||||
api.add_route("/graph", view)
|
api.add_route("/graph", view)
|
||||||
|
|
||||||
@@ -58,12 +133,18 @@ 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.
|
||||||
|
|
||||||
New in Responder `1.4.0`::
|
.. note::
|
||||||
|
|
||||||
|
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.schema import Schema as OpenAPISchema
|
from responder.ext.openapi import OpenAPISchema
|
||||||
from marshmallow import Schema, fields
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
contact = {
|
contact = {
|
||||||
@@ -194,12 +275,11 @@ 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 = Schema(
|
schema = OpenAPISchema(
|
||||||
app=api,
|
app=api,
|
||||||
title="Web Service",
|
title="Web Service",
|
||||||
version="1.0",
|
version="1.0",
|
||||||
@@ -214,7 +294,7 @@ The new and recommended way::
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
The old way ::
|
The old way::
|
||||||
|
|
||||||
api = responder.API(
|
api = responder.API(
|
||||||
title="Web Service",
|
title="Web Service",
|
||||||
@@ -283,7 +363,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#Directives>`_
|
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_
|
||||||
|
|
||||||
|
|
||||||
Using Cookie-Based Sessions
|
Using Cookie-Based Sessions
|
||||||
@@ -353,7 +433,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: **Requests**.
|
Responder comes with a first-class, well supported test client for your ASGI web services (powered by Starlette's TestClient).
|
||||||
|
|
||||||
Here's an example of a test (written with pytest)::
|
Here's an example of a test (written with pytest)::
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 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()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 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()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 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
@@ -0,0 +1,183 @@
|
|||||||
|
[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
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[pytest]
|
|
||||||
;addopts= -rsxX -s -v --strict
|
|
||||||
filterwarnings =
|
|
||||||
error::UserWarning
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
build:
|
|
||||||
image: latest
|
|
||||||
|
|
||||||
python:
|
|
||||||
version: 3.6
|
|
||||||
+17
-1
@@ -1,2 +1,18 @@
|
|||||||
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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .cli import main
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.0.4"
|
__version__ = "3.1.0"
|
||||||
|
|||||||
+161
-75
@@ -1,41 +1,37 @@
|
|||||||
import json
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import jinja2
|
__all__ = ["API"]
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from starlette.exceptions import ExceptionMiddleware
|
|
||||||
from starlette.middleware.wsgi import WSGIMiddleware
|
|
||||||
from starlette.middleware.errors import ServerErrorMiddleware
|
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.middleware.errors import ServerErrorMiddleware
|
||||||
|
from starlette.middleware.exceptions import ExceptionMiddleware
|
||||||
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.trustedhost import TrustedHostMiddleware
|
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from starlette.routing import Lifespan
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||||
from starlette.staticfiles import StaticFiles
|
|
||||||
from starlette.testclient import TestClient
|
|
||||||
from starlette.websockets import WebSocket
|
|
||||||
|
|
||||||
from . import models, status_codes
|
from . import 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
|
||||||
|
|
||||||
@@ -44,13 +40,12 @@ class API:
|
|||||||
*,
|
*,
|
||||||
debug=False,
|
debug=False,
|
||||||
title=None,
|
title=None,
|
||||||
version="1.0",
|
version=None,
|
||||||
description=None,
|
description=None,
|
||||||
terms_of_service=None,
|
terms_of_service=None,
|
||||||
contact=None,
|
contact=None,
|
||||||
license=None,
|
license=None, # noqa: A002
|
||||||
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",
|
||||||
@@ -62,17 +57,19 @@ 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()
|
self.router = Router(lifespan=lifespan)
|
||||||
|
|
||||||
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_dir
|
static_route = ""
|
||||||
static_dir = Path(os.path.abspath(static_dir))
|
static_dir = Path(static_dir).resolve()
|
||||||
|
|
||||||
self.static_dir = static_dir
|
self.static_dir = static_dir
|
||||||
self.static_route = static_route
|
self.static_route = static_route
|
||||||
@@ -83,22 +80,15 @@ 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:
|
||||||
os.makedirs(self.static_dir, exist_ok=True)
|
self.static_dir.mkdir(parents=True, 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
|
||||||
@@ -116,11 +106,19 @@ 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_version,
|
openapi=openapi,
|
||||||
docs_route=docs_route,
|
docs_route=docs_route,
|
||||||
description=description,
|
description=description,
|
||||||
terms_of_service=terms_of_service,
|
terms_of_service=terms_of_service,
|
||||||
@@ -128,13 +126,15 @@ 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 = (
|
|
||||||
self.session()
|
@property
|
||||||
) #: A Requests session that is connected to the ASGI app.
|
def requests(self):
|
||||||
|
"""A test client connected to the ASGI app. Lazily initialized."""
|
||||||
|
return self.session()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def static_app(self):
|
def static_app(self):
|
||||||
@@ -153,9 +153,59 @@ 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 schema(self, name, **options):
|
def exception_handler(self, exception_cls):
|
||||||
"""Decorator for creating new routes around function and class definitions.
|
"""Register a handler for a specific exception type.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
|
@api.exception_handler(ValueError)
|
||||||
|
async def handle_value_error(req, resp, exc):
|
||||||
|
resp.status_code = 400
|
||||||
|
resp.media = {"error": str(exc)}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
async def _handler(request, exc):
|
||||||
|
from starlette.responses import Response as StarletteResp
|
||||||
|
|
||||||
|
req = Request(request.scope, request.receive, formats=get_formats())
|
||||||
|
resp = Response(req=req, formats=get_formats())
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
await func(req, resp, exc)
|
||||||
|
else:
|
||||||
|
func(req, resp, exc)
|
||||||
|
if resp.status_code is None:
|
||||||
|
resp.status_code = 500
|
||||||
|
body, headers = await resp.body
|
||||||
|
return StarletteResp(
|
||||||
|
content=body, status_code=resp.status_code, headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register with the ExceptionMiddleware
|
||||||
|
self.router._exception_handlers = getattr(
|
||||||
|
self.router, "_exception_handlers", {}
|
||||||
|
)
|
||||||
|
self.router._exception_handlers[exception_cls] = _handler
|
||||||
|
# Also register on the ASGI app chain
|
||||||
|
from starlette.middleware.exceptions import ExceptionMiddleware as EM
|
||||||
|
|
||||||
|
app = self.app
|
||||||
|
while app is not None:
|
||||||
|
if isinstance(app, EM):
|
||||||
|
app.add_exception_handler(exception_cls, _handler)
|
||||||
|
break
|
||||||
|
app = getattr(app, "app", None)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def schema(self, name, **options):
|
||||||
|
"""
|
||||||
|
Decorator for creating new routes around function and class definitions.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
from marshmallow import Schema, fields
|
from marshmallow import Schema, fields
|
||||||
@api.schema("Pet")
|
@api.schema("Pet")
|
||||||
class PetSchema(Schema):
|
class PetSchema(Schema):
|
||||||
@@ -172,11 +222,12 @@ 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,
|
||||||
@@ -188,16 +239,18 @@ 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", and it will become a default route.
|
:param static: If ``True``, and no endpoint was passed, render "static/index.html".
|
||||||
"""
|
Also, it will become a default route.
|
||||||
|
:param methods: Optional list of HTTP methods (e.g. ``["GET", "POST"]``).
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -211,27 +264,35 @@ 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 os.path.exists(index):
|
if index.exists():
|
||||||
with open(index, "r") as f:
|
resp.html = index.read_text()
|
||||||
resp.html = f.read()
|
|
||||||
else:
|
else:
|
||||||
resp.status_code = status_codes.HTTP_404
|
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined]
|
||||||
resp.text = "Not found."
|
resp.text = "Not found."
|
||||||
|
|
||||||
def redirect(
|
def redirect(
|
||||||
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
|
self,
|
||||||
|
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, representing the HTTP status code of the redirect.
|
:param status_code: an `API.status_codes` attribute, or an integer,
|
||||||
|
representing the HTTP status code of the redirect.
|
||||||
"""
|
"""
|
||||||
resp.redirect(location, set_text=set_text, status_code=status_code)
|
resp.redirect(location, set_text=set_text, status_code=status_code)
|
||||||
|
|
||||||
@@ -264,7 +325,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.lifespan_handler.add_event_handler(event_type, handler)
|
self.router.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.
|
||||||
@@ -283,26 +344,50 @@ 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 (shouldn't be parameterized).
|
:param route: String representation of the route to be used
|
||||||
|
(shouldn't be parameterized).
|
||||||
:param app: The other WSGI / ASGI app.
|
: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 Requests session object, able to send HTTP requests to the Responder application.
|
"""Testing HTTP client. Returns a Starlette TestClient instance,
|
||||||
|
able to send HTTP requests to the Responder application.
|
||||||
|
|
||||||
:param base_url: The URL to mount the connection adaptor to.
|
:param base_url: The base URL for the test client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.
|
||||||
@@ -311,48 +396,49 @@ 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):
|
||||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
|
r"""Render a Jinja2 template file with the provided values.
|
||||||
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: Date to pass into the template.
|
:param \*\*kwargs: Data 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):
|
||||||
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
|
r"""Render a Jinja2 template string with the provided values.
|
||||||
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
|
|
||||||
:param source: The template to use.
|
:param source: The template to use, a Jinja2 template string.
|
||||||
: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
|
"""
|
||||||
variable is set, requests will be served on that port automatically to all
|
Run the application with uvicorn.
|
||||||
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: Run uvicorn server in debug mode.
|
:param debug: Whether to run application 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"
|
address = "0.0.0.0" # noqa: S104
|
||||||
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"
|
||||||
|
|
||||||
def spawn():
|
uvicorn.run(self, host=address, port=port, **options)
|
||||||
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:
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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):
|
||||||
@@ -16,9 +18,6 @@ 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
|
||||||
@@ -27,7 +26,7 @@ class BackgroundQueue:
|
|||||||
def on_future_done(fs):
|
def on_future_done(fs):
|
||||||
try:
|
try:
|
||||||
fs.result()
|
fs.result()
|
||||||
except:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
def do_task(*args, **kwargs):
|
def do_task(*args, **kwargs):
|
||||||
@@ -39,6 +38,5 @@ 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.ensure_future(func(*args, **kwargs))
|
return await asyncio.create_task(func(*args, **kwargs))
|
||||||
else:
|
return await run_in_threadpool(func, *args, **kwargs)
|
||||||
return await run_in_threadpool(func, *args, **kwargs)
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
"""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()
|
|
||||||
+6
-1
@@ -1,3 +1,8 @@
|
|||||||
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 +0,0 @@
|
|||||||
from .graphql import GraphQLView
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
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"
|
||||||
|
)
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -12,24 +9,19 @@ class GraphQLView:
|
|||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _resolve_graphql_query(req):
|
async def _resolve_graphql_query(req, resp):
|
||||||
# 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
|
||||||
@@ -37,34 +29,38 @@ 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).
|
||||||
# TODO: Make some assertions about content-type here.
|
return await req.text, None, None
|
||||||
return req.text, None, None
|
|
||||||
|
|
||||||
async def graphql_response(self, req, resp, schema):
|
async def graphql_response(self, req, resp):
|
||||||
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
|
return None
|
||||||
|
|
||||||
|
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 = schema.execute(
|
result = self.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(
|
|
||||||
[result],
|
response_data = {}
|
||||||
is_batch=False,
|
if result.errors:
|
||||||
format_error=default_format_error,
|
response_data["errors"] = [{"message": str(e)} for e in result.errors]
|
||||||
encode=partial(json_encode, pretty=False),
|
if result.data is not None:
|
||||||
)
|
response_data["data"] = result.data
|
||||||
resp.media = json.loads(result)
|
|
||||||
return (query, result, status_code)
|
resp.media = response_data
|
||||||
|
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, self.schema)
|
await self.graphql_response(req, resp)
|
||||||
|
|
||||||
async def __call__(self, req, resp):
|
async def __call__(self, req, resp):
|
||||||
await self.on_request(req, resp)
|
await self.on_request(req, resp)
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
|
# ruff: noqa: E501
|
||||||
GRAPHIQL = """
|
GRAPHIQL = """
|
||||||
{% set GRAPHIQL_VERSION = '0.12.0' %}
|
{% set GRAPHIQL_VERSION = '3.0.6' %}
|
||||||
|
{% 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>
|
||||||
@@ -22,123 +17,17 @@ 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'));
|
||||||
* This GraphiQL example illustrates how to use some of GraphiQL's props
|
root.render(React.createElement(GraphiQL, { fetcher: fetcher }));
|
||||||
* 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>
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
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 Schema:
|
class OpenAPISchema:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
app,
|
app,
|
||||||
@@ -22,11 +18,12 @@ class Schema:
|
|||||||
description=None,
|
description=None,
|
||||||
terms_of_service=None,
|
terms_of_service=None,
|
||||||
contact=None,
|
contact=None,
|
||||||
license=None,
|
license=None, # noqa: A002
|
||||||
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 = {}
|
||||||
@@ -40,7 +37,9 @@ class Schema:
|
|||||||
self.openapi_version = openapi
|
self.openapi_version = openapi
|
||||||
self.openapi_route = openapi_route
|
self.openapi_route = openapi_route
|
||||||
|
|
||||||
self.docs_theme = DEFAULT_API_THEME
|
self.docs_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
|
||||||
@@ -51,17 +50,13 @@ class Schema:
|
|||||||
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 = (
|
theme_path = (Path(__file__).parent / "docs").resolve()
|
||||||
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
|
self.templates = Templates(directory=theme_path)
|
||||||
).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
|
||||||
@@ -82,9 +77,7 @@ class Schema:
|
|||||||
|
|
||||||
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(
|
operations = yaml_utils.load_operations_from_docstring(route.description)
|
||||||
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():
|
||||||
@@ -97,7 +90,7 @@ class Schema:
|
|||||||
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 mashmallow schema to the API specification."""
|
"""Adds a marshmallow schema to the API specification."""
|
||||||
if check_existing:
|
if check_existing:
|
||||||
assert name not in self.schemas
|
assert name not in self.schemas
|
||||||
|
|
||||||
@@ -124,25 +117,10 @@ class Schema:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def docs(self):
|
def docs(self):
|
||||||
|
return self.templates.render(
|
||||||
loader = jinja2.PrefixLoader(
|
f"{self.docs_theme}.html",
|
||||||
{
|
title=self.title,
|
||||||
self.docs_theme: jinja2.PackageLoader(
|
version=self.version,
|
||||||
"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",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,6 +133,6 @@ class Schema:
|
|||||||
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
|
resp.status_code = status_codes.HTTP_200 # type: ignore[attr-defined]
|
||||||
resp.headers["Content-Type"] = "application/x-yaml"
|
resp.headers["Content-Type"] = "application/x-yaml"
|
||||||
resp.content = self.openapi
|
resp.content = self.openapi
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!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>
|
||||||
+101
-43
@@ -1,84 +1,142 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from requests_toolbelt.multipart import decoder
|
from python_multipart import MultipartParser
|
||||||
|
|
||||||
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:
|
||||||
pass
|
return None
|
||||||
elif "multipart/form-data" in r.headers.get("Content-Type"):
|
if "multipart/form-data" in r.headers.get("Content-Type"):
|
||||||
decode = decoder.MultipartDecoder(await r.content, r.mimetype)
|
parts = _parse_multipart(await r.content, r.mimetype)
|
||||||
querys = list()
|
queries = []
|
||||||
for part in decode.parts:
|
for part in parts:
|
||||||
header = part.headers.get(b"Content-Disposition").decode("utf-8")
|
header = part.headers.get("Content-Disposition", "")
|
||||||
text = part.text
|
text = part.body.decode("utf-8")
|
||||||
|
|
||||||
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]
|
||||||
querys.append((key, text))
|
queries.append((key, text))
|
||||||
|
|
||||||
content = urlencode(querys)
|
content = urlencode(queries)
|
||||||
return QueryDict(content)
|
return QueryDict(content)
|
||||||
else:
|
return QueryDict(await r.text)
|
||||||
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)
|
||||||
else:
|
return yaml.safe_load(await r.content)
|
||||||
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)
|
||||||
else:
|
return json.loads(await r.content)
|
||||||
return json.loads(await r.content)
|
|
||||||
|
|
||||||
|
|
||||||
async def format_files(r, encode=False):
|
async def format_files(r, encode=False):
|
||||||
if encode:
|
if encode:
|
||||||
pass
|
return None
|
||||||
else:
|
parts = _parse_multipart(await r.content, r.mimetype)
|
||||||
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
|
dump = {}
|
||||||
dump = {}
|
for part in parts:
|
||||||
for part in decoded.parts:
|
header = part.headers.get("Content-Disposition", "")
|
||||||
header = part.headers[b"Content-Disposition"].decode("utf-8")
|
mimetype = part.headers.get("Content-Type", None)
|
||||||
mimetype = part.headers.get(b"Content-Type", None)
|
filename = None
|
||||||
filename = None
|
formname = 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]
|
||||||
|
|
||||||
value = value[1:-1]
|
if key == "filename":
|
||||||
|
filename = value
|
||||||
|
elif key == "name":
|
||||||
|
formname = value
|
||||||
|
|
||||||
if key == "filename":
|
if formname is None:
|
||||||
filename = value
|
continue
|
||||||
elif key == "name":
|
|
||||||
formname = value
|
|
||||||
|
|
||||||
if mimetype is None:
|
if mimetype is None:
|
||||||
dump[formname] = part.content
|
dump[formname] = part.body
|
||||||
else:
|
else:
|
||||||
dump[formname] = {
|
dump[formname] = {
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"content": part.content,
|
"content": part.body,
|
||||||
"content-type": mimetype.decode("utf-8"),
|
"content-type": mimetype,
|
||||||
}
|
}
|
||||||
return dump
|
return dump
|
||||||
|
|
||||||
|
|
||||||
def get_formats():
|
def get_formats():
|
||||||
|
|||||||
+127
-41
@@ -1,29 +1,51 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import io
|
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
from collections.abc import Callable
|
||||||
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
|
||||||
|
|
||||||
import chardet
|
__all__ = ["Request", "Response", "QueryDict"]
|
||||||
import rfc3986
|
|
||||||
import graphene
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from requests.structures import CaseInsensitiveDict
|
try:
|
||||||
from requests.cookies import RequestsCookieJar
|
import chardet
|
||||||
|
except ImportError:
|
||||||
from starlette.datastructures import MutableHeaders
|
chardet = None # type: ignore[assignment]
|
||||||
from starlette.requests import Request as StarletteRequest, State
|
from starlette.requests import Request as StarletteRequest
|
||||||
|
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):
|
||||||
@@ -112,7 +134,7 @@ class Request:
|
|||||||
self.api = api
|
self.api = api
|
||||||
self._content = None
|
self._content = None
|
||||||
|
|
||||||
headers = CaseInsensitiveDict()
|
headers: CaseInsensitiveDict = CaseInsensitiveDict()
|
||||||
for key, value in self._starlette.headers.items():
|
for key, value in self._starlette.headers.items():
|
||||||
headers[key] = value
|
headers[key] = value
|
||||||
|
|
||||||
@@ -133,6 +155,11 @@ 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."""
|
||||||
@@ -146,20 +173,20 @@ class Request:
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
"""The parsed URL of the Request."""
|
"""The parsed URL of the Request."""
|
||||||
return rfc3986.urlparse(self.full_url)
|
return 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 = RequestsCookieJar()
|
cookies = {}
|
||||||
cookie_header = self.headers.get("Cookie", "")
|
cookie_header = self.headers.get("Cookie", "")
|
||||||
|
|
||||||
bc = SimpleCookie(cookie_header)
|
bc: SimpleCookie = 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.get_dict()
|
self._cookies = cookies
|
||||||
|
|
||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
@@ -171,6 +198,16 @@ 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:
|
||||||
"""
|
"""
|
||||||
@@ -213,16 +250,23 @@ 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, provided by the chardet library. Must be awaited."""
|
"""The apparent encoding, detected automatically. 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
|
||||||
|
|
||||||
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
|
if chardet is not None:
|
||||||
|
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
|
||||||
|
|
||||||
|
return DEFAULT_ENCODING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_secure(self):
|
def is_secure(self):
|
||||||
@@ -232,20 +276,31 @@ 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=None):
|
async def media(self, format: str | Callable = None): # noqa: A002
|
||||||
"""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. Alternatively accepts a custom callable for the format type.
|
:param format: The name of the format being used.
|
||||||
|
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"
|
format = "yaml" if "yaml" in self.mimetype or "" else "json" # noqa: A001
|
||||||
format = "form" if "form" in self.mimetype or "" else format
|
format = "form" if "form" in self.mimetype or "" else format # noqa: A001
|
||||||
|
|
||||||
|
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:
|
||||||
return await format(self)
|
raise TypeError(f"Invalid 'format' argument: {format}")
|
||||||
|
|
||||||
|
return await formatter(self)
|
||||||
|
|
||||||
|
|
||||||
def content_setter(mimetype):
|
def content_setter(mimetype):
|
||||||
@@ -279,22 +334,22 @@ class Response:
|
|||||||
|
|
||||||
def __init__(self, req, *, formats):
|
def __init__(self, req, *, formats):
|
||||||
self.req = req
|
self.req = req
|
||||||
self.status_code = None #: The HTTP Status Code to use for the Response.
|
#: 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 sent back to the client. Typically, in JSON formatting.
|
self.media = None #: A Python object that will be content-negotiated and
|
||||||
|
#: sent back to the client. Typically, in JSON formatting.
|
||||||
self._stream = None
|
self._stream = None
|
||||||
self.headers = (
|
self.headers = {} #: A Python dictionary of ``{key: value}``,
|
||||||
{}
|
#: 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() #: The cookies set in the Response
|
self.cookies: SimpleCookie = 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)
|
||||||
|
|
||||||
@@ -302,6 +357,25 @@ 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:
|
||||||
@@ -320,12 +394,13 @@ 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
|
||||||
content = content.encode(self.encoding)
|
if isinstance(content, str):
|
||||||
|
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 (
|
||||||
@@ -369,12 +444,23 @@ 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, headers=headers)
|
response = response_cls(body, status_code=self.status_code_safe, 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
|
||||||
|
|||||||
+104
-37
@@ -1,24 +1,28 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
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 .statics import DEFAULT_SESSION_COOKIE
|
from .models import Request, Response
|
||||||
|
|
||||||
|
_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_]*)?}")
|
||||||
@@ -32,9 +36,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 (
|
assert convertor_type in _CONVERTORS.keys(), (
|
||||||
convertor_type in _CONVERTORS.keys()
|
f"Unknown path convertor '{convertor_type}'"
|
||||||
), 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()]
|
||||||
@@ -58,19 +62,22 @@ class BaseRoute:
|
|||||||
|
|
||||||
|
|
||||||
class Route(BaseRoute):
|
class Route(BaseRoute):
|
||||||
def __init__(self, route, endpoint, *, before_request=False):
|
def __init__(self, route, endpoint, *, before_request=False, methods=None):
|
||||||
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.route.format(**params)
|
return self._url_template.format(**params)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint_name(self):
|
def endpoint_name(self):
|
||||||
@@ -84,6 +91,9 @@ 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)
|
||||||
|
|
||||||
@@ -108,6 +118,10 @@ 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 = []
|
||||||
|
|
||||||
@@ -121,14 +135,14 @@ class Route(BaseRoute):
|
|||||||
try:
|
try:
|
||||||
view = getattr(endpoint, method_name)
|
view = getattr(endpoint, method_name)
|
||||||
views.append(view)
|
views.append(view)
|
||||||
except AttributeError:
|
except AttributeError as ex:
|
||||||
if on_request is None:
|
if on_request is None:
|
||||||
raise HTTPException(status_code=status_codes.HTTP_405)
|
raise HTTPException(status_code=status_codes.HTTP_405) from ex # type: ignore[attr-defined]
|
||||||
else:
|
else:
|
||||||
views.append(self.endpoint)
|
views.append(self.endpoint)
|
||||||
|
|
||||||
for view in views:
|
for view in views:
|
||||||
# "Monckey patch" for graphql: explicitly checking __call__
|
# Check __call__ for class-based views (e.g. GraphQL)
|
||||||
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
|
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
|
||||||
view.__call__
|
view.__call__
|
||||||
):
|
):
|
||||||
@@ -137,12 +151,11 @@ 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
|
response.status_code = status_codes.HTTP_200 # type: ignore[attr-defined]
|
||||||
|
|
||||||
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):
|
||||||
@@ -157,12 +170,13 @@ 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.route.format(**params)
|
return self._url_template.format(**params)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint_name(self):
|
def endpoint_name(self):
|
||||||
@@ -198,7 +212,6 @@ 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):
|
||||||
@@ -206,17 +219,20 @@ class WebSocketRoute(BaseRoute):
|
|||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
def __init__(self, routes=None, default_response=None, before_requests=None):
|
def __init__(
|
||||||
|
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 = {}
|
self.apps: dict[str, ASGIApp] = {}
|
||||||
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,
|
||||||
@@ -227,11 +243,13 @@ 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:
|
||||||
@@ -241,9 +259,9 @@ class Router:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if check_existing:
|
if check_existing:
|
||||||
assert not self.routes or route not in (
|
assert not self.routes or route not in (item.route for item in self.routes), (
|
||||||
item.route for item in self.routes
|
f"Route '{route}' already exists"
|
||||||
), f"Route '{route}' already exists"
|
)
|
||||||
|
|
||||||
if default:
|
if default:
|
||||||
self.default_endpoint = endpoint
|
self.default_endpoint = endpoint
|
||||||
@@ -251,14 +269,27 @@ class Router:
|
|||||||
if websocket:
|
if websocket:
|
||||||
route = WebSocketRoute(route, endpoint)
|
route = WebSocketRoute(route, endpoint)
|
||||||
else:
|
else:
|
||||||
route = Route(route, endpoint)
|
route = Route(route, endpoint, methods=methods)
|
||||||
|
|
||||||
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:
|
||||||
@@ -267,7 +298,6 @@ 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)
|
||||||
@@ -276,13 +306,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(receive, send)
|
await websocket_close(scope, receive, send)
|
||||||
return
|
return
|
||||||
|
|
||||||
request = Request(scope, receive)
|
request = Request(scope, receive)
|
||||||
response = Response(request, formats=get_formats())
|
response = Response(request, formats=get_formats()) # noqa: F841
|
||||||
|
|
||||||
raise HTTPException(status_code=status_codes.HTTP_404)
|
raise HTTPException(status_code=status_codes.HTTP_404) # type: ignore[attr-defined]
|
||||||
|
|
||||||
def _resolve_route(self, scope):
|
def _resolve_route(self, scope):
|
||||||
for route in self.routes:
|
for route in self.routes:
|
||||||
@@ -292,11 +322,46 @@ 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_handler(scope, receive, send)
|
await self.lifespan(scope, receive, send)
|
||||||
return
|
return
|
||||||
|
|
||||||
path = scope["path"]
|
path = scope["path"]
|
||||||
@@ -320,8 +385,10 @@ 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_response(scope, receive, send)
|
await self.default_endpoint(scope, receive, send)
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import typing
|
from starlette.staticfiles import StaticFiles as StarletteStaticFiles
|
||||||
|
|
||||||
from starlette.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
|
|
||||||
class StaticFiles(StaticFiles):
|
class StaticFiles(StarletteStaticFiles):
|
||||||
"""I've created an issue to disccuss allowing multiple directories in starletter's `StaticFiles`.
|
"""Extension to Starlette's StaticFiles with support for multiple directories."""
|
||||||
|
|
||||||
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)]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
API_THEMES = ["elements", "rapidoc", "redoc", "swagger_ui"]
|
||||||
DEFAULT_ENCODING = "utf-8"
|
DEFAULT_ENCODING = "utf-8"
|
||||||
DEFAULT_API_THEME = "swaggerui"
|
DEFAULT_OPENAPI_THEME = "swagger_ui"
|
||||||
DEFAULT_SESSION_COOKIE = "Responder-Session"
|
DEFAULT_SESSION_COOKIE = "Responder-Session"
|
||||||
DEFAULT_SECRET_KEY = "NOTASECRET"
|
DEFAULT_SECRET_KEY = "NOTASECRET" # noqa: S105
|
||||||
|
|
||||||
DEFAULT_CORS_PARAMS = {
|
DEFAULT_CORS_PARAMS = {
|
||||||
"allow_origins": (),
|
"allow_origins": (),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# from: https://github.com/requests/requests/blob/master/requests/status_codes.py
|
|
||||||
|
|
||||||
codes = {
|
codes = {
|
||||||
# Informational.
|
# Informational.
|
||||||
100: ("continue",),
|
100: ("continue",),
|
||||||
@@ -26,11 +24,7 @@ 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: (
|
308: ("permanent_redirect",),
|
||||||
"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",),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from contextlib import contextmanager
|
|||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
|
__all__ = ["Templates"]
|
||||||
|
|
||||||
|
|
||||||
class Templates:
|
class Templates:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -10,7 +12,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,
|
autoescape=autoescape, # noqa: S701
|
||||||
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}
|
||||||
@@ -33,7 +35,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
|
||||||
@@ -54,6 +56,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)
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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
Reference in New Issue
Block a user