Compare commits

..

149 Commits

Author SHA1 Message Date
kennethreitz 0cbcaf9c4f Code quality improvements and test fixes (#592)
## Summary
Comprehensive post-v3.0.0 modernization: new features, bug fixes,
dependency cleanup, docs, and test coverage.

**New features:**
- **HTTP method filtering** — `@api.route("/data", methods=["GET"])`
- **Lifespan context manager** — modern async startup/shutdown
- **`api.exception_handler()`** — custom error handling per exception
type
- **`api.graphql()`** — one-liner GraphQL setup
- **`resp.file()`** — serve files from disk with auto content-type
- **before_request short-circuit** — set status code to skip route
handler
- **`req.path_params`** / **`req.client`** / **`req.is_json`** — new
request properties
- **`uuid`** and **`path`** route convertors
- **PEP 561 `py.typed`** marker

**Bug fixes:**
- Fix multipart parser losing headers
- Fix `url_for()` with typed params (`{id:int}`)
- Fix `resp.body` encoding crash on bytes content
- Fix Python 3.9 type syntax (`from __future__ import annotations`)
- Fix broken session test and no-op file upload test
- Fix helloworld example 404 on root path

**Dependencies:**
- Flattened — `pip install responder` gets everything
- Core: just starlette + uvicorn (down from 10 deps)

**Docs & README:**
- All new features documented in tour
- Modernized README features list
- Deployment guide: Docker, cloud, uvicorn
- Removed Pipenv, extras, stale references throughout

**Tests & quality:**
- 117 tests (up from 92), 91% coverage, 0 warnings
- CaseInsensitiveDict, GraphQL edge cases, staticfiles tests
- Ruff clean, all `tmpdir` → `tmp_path`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 07:44:11 -04:00
kennethreitz 3fa6f11ffa Clean up stale comments, dead test code, and flaky npm assertions
- Remove commented-out tests (route overlap, form data, file uploads)
- Remove stale TODO/FIXME comments from routes.py and api.py
- Make CLI npm error assertions case-insensitive and more flexible
  to handle different npm versions across CI environments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8b88b148bf Remove requests/requests-toolbelt from test code
Replace with stdlib urllib.request. No third-party HTTP client
needed for test probing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 1aecafa82a Update CHANGELOG for v3.0.0 release
Reflect all changes made in this branch: dependency reduction,
GraphQL modernization, pyproject.toml migration, etc. Also fix
compare links to point to kennethreitz/responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 8c763aa97e Migrate from setup.py to declarative pyproject.toml
All package metadata now lives in pyproject.toml. Removes setup.py
and MANIFEST.in. Also exports __version__ from the package root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 91aa242a5a Fix python-multipart import deprecation warning
Use `python_multipart` import name instead of deprecated `multipart`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 084d057a99 Move apispec and marshmallow to openapi extra
These are only used by the OpenAPI extension, not core responder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz d3acf2c1c1 Drop rfc3986 dep, clean up internals
- Replace rfc3986 with stdlib urllib.parse
- Remove deprecated status code aliases (resume_incomplete/resume)
  that were marked for removal in 3.0
- Remove private ThreadPoolExecutor API usage in BackgroundQueue
- Clean up stale comments (old Starlette PR refs, requests attribution)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 80715a12ac Drop requests dependency, modernize GraphQL, clean up setup.py
- Remove `requests` as a core dependency — replaced CaseInsensitiveDict
  and RequestsCookieJar with lightweight stdlib implementations
- Remove `requests-toolbelt` — replaced multipart decoder with
  python-multipart (already a starlette transitive dep)
- Upgrade GraphQL to graphene 3 + graphql-core 3, drop graphql-server-core
- Update GraphiQL template from 0.12.0 (2018) to 3.0.6 with React 18
- Clean up setup.py: remove dead DebCommand, UploadCommand, publish hack
- Remove linting from `poe check` (tests only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz 66fc7afbe4 CI: Simplify test matrix to Ubuntu-only
No need to test on all three OSes — this is a pure Python web
framework with no platform-specific code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
kennethreitz e7776eb9e8 v3.0.0: Modernize for latest Starlette, drop EOL Pythons
- Bump version to 3.0.0
- Replace deprecated starlette.middleware.wsgi with a2wsgi
- Pin starlette[full]>=0.40, add a2wsgi dependency
- Bump minimum Python to 3.9, drop 3.7/3.8 support
- Remove whitenoise conditional (Python <3.8 no longer supported)
- Clean up CI matrix: drop old Python versions and OS exclusions
- Fix deprecated httpx TestClient usage in tests (data= -> content=, per-request cookies)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:51:47 -04:00
Andreas Motl 944d47da45 CI: Remove pip caching. Has woes. 2025-02-03 00:43:10 +01:00
Andreas Motl a3a12cff77 Chore: Add another basic example 2025-02-03 00:43:10 +01:00
Andreas Motl 7b2839086d Thanks, JetBrains. 2025-01-21 23:19:09 +01:00
Andreas Motl 351ff8d95e Documentation: Copy editing, this and that 2025-01-19 05:21:46 +01:00
Andreas Motl 2278beba18 SFA: Update to pueblo[sfa] 0.0.11 2025-01-19 05:02:47 +01:00
Andreas Motl 3cfc7ec2b6 Chore: Remove support for EOL Python 3.6 2025-01-19 05:02:47 +01:00
Andreas Motl 0de22eeed2 SFA: Use application loader from pueblo.sfa 2025-01-19 05:02:47 +01:00
Andreas Motl b0cc37861b SFA: Unlock loading application from remote location, using fsspec 2025-01-19 05:02:47 +01:00
Andreas Motl 7d4532acc9 CI: Use GHA recipe astral-sh/setup-uv, and more 2025-01-18 22:56:50 +01:00
Andreas Motl 1b63d2943a Chore: A few updates from code review etc. 2025-01-18 22:22:36 +01:00
Andreas Motl b5723303c8 CI: The macOS-12 environment is deprecated 2025-01-18 22:22:36 +01:00
Andreas Motl 5730be4b31 Chore: Format code using most recent ruff and pyproject-fmt 2025-01-18 22:22:36 +01:00
Andreas Motl 6f9c11645a CLI: Load from file or module. Add software tests and documentation.
Also, refactor to `responder.ext.cli`.
2025-01-18 22:22:36 +01:00
kennethreitz 827cc64988 CLI: Re-add command line interface (2024)
Install: pip install 'responder[cli]'

The CLI is an optional subsystem from now on.
2025-01-18 22:22:36 +01:00
Andreas Motl 7b5db5bc33 uvicorn: Fix uvicorn.run invocation re. debug argument
The `debug` argument no longer exists. Let's adjust the `log_level` to
`debug` instead.
2025-01-18 22:22:36 +01:00
kennethreitz b9a03c7088 Create FUNDING.yml
Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
2024-11-17 06:28:31 -05:00
Andreas Motl 4cbf55508e Documentation: Update change log for upcoming version 3.0.0 2024-10-31 09:51:29 +01:00
Andreas Motl 83d0fcf1ae Documentation: Update developer sandbox documentation 2024-10-31 09:51:29 +01:00
Taoufik a698eaaab3 GraphQL: Re-add extension and dependencies (2024) 2024-10-31 06:36:13 +01:00
Andreas Motl 3aa21eed08 OpenAPI: Refactor module to responder.ext.openapi
It has been `responder.ext.schema` before.
2024-10-30 23:45:55 +01:00
Andreas Motl 2741c74b90 OpenAPI: Make extension optional
Install with: pip install 'responder[openapi]'
2024-10-30 23:45:55 +01:00
Andreas Motl aba96525ad Dependencies: Migrate from WhiteNoise to ServeStatic 2024-10-30 23:21:23 +01:00
Andreas Motl a5b6d36991 Sandbox: Enable mypy type checker 2024-10-30 23:12:11 +01:00
Andreas Motl e4cff76fa6 Documentation: Unlock writing in Markdown, using Sphinx/MyST 2024-10-30 21:23:12 +01:00
Andreas Motl f11ad7136d Documentation: Add Sphinx extensions "copybutton" and "opengraph" 2024-10-30 21:23:12 +01:00
Andreas Motl c32e8c7468 Documentation: Refactor Sphinx dependencies into setup.py 2024-10-30 20:42:10 +01:00
Andreas Motl d93e3cd12c Documentation: Update Read the Docs (RTD) configuration 2024-10-30 20:42:10 +01:00
Andreas Motl 040f1a57e4 Dependencies: Remove aiofiles
Apparently, it is not used.
2024-10-28 16:36:46 +01:00
dependabot[bot] 307313744f Update alabaster requirement from <0.8 to <1.1
Updates the requirements on [alabaster](https://github.com/sphinx-doc/alabaster) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/alabaster/releases)
- [Changelog](https://github.com/sphinx-doc/alabaster/blob/master/docs/changelog.rst)
- [Commits](https://github.com/sphinx-doc/alabaster/compare/0.1.0...1.0.0)

---
updated-dependencies:
- dependency-name: alabaster
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 10:47:51 +01:00
Andreas Motl 98ca45003b Documentation: Badges, linking, wording, inline comments. This and that.
A few of the adjustments here have been required to mitigate Sphinx
warnings, which would converge to errors on CI, thus failing the build.

A few other changes, both wording and syntax/formatting fixes, are
coming from regular copyediting and documentation maintenance.
2024-10-27 18:13:13 +01:00
Andreas Motl ab76594297 CI: Run link checker and build documentation as GHA workflow 2024-10-27 18:13:13 +01:00
Andreas Motl 7fba0f6362 Fix dispatching static_route=None on Windows 2024-10-26 05:20:57 -04:00
Andreas Motl 4ff73e9d0c Sandbox: Bring back python setup.py publish subcommand
It has been removed too early.
2024-10-26 05:16:12 -04:00
Andreas Motl 68bbea0a55 CI: Validate on Python 3.6+
You never know how you possibly save someone's life with that.
2024-10-26 00:42:07 +02:00
Andreas Motl 106e5e9073 CI: Validate on Windows operating system 2024-10-26 00:27:44 +02:00
Andreas Motl 3426aa71da Documentation: Fix broken links in README 2024-10-25 12:13:08 -04:00
Andreas Motl 413028b636 Tasks: Define sandbox tasks in pyproject.toml, using poethepoet
The fundamental commands to mostly use are:

- poe format
- poe check
2024-10-25 07:39:54 -04:00
Andreas Motl 3edf979a8c Dependencies: Dissolve requirements-dev.txt 2024-10-25 07:39:54 -04:00
Andreas Motl cd75deeb4e Python: Verify support for Python 3.13 2024-10-24 18:36:05 +02:00
Andreas Motl b71bb5ddb9 apistar: Rename variables api_theme -> openapi_theme, etc. 2024-10-24 18:19:03 +02:00
Tabot Kevin 27a9459f22 apistar: Replace use of apistar package with local API theme files
This has already been submitted by @tabotkevin with GH-480, but got lost
for whatever reason.
2024-10-24 18:19:03 +02:00
dependabot[bot] b39c539d57 Update readme-renderer requirement from <23 to <45 (#540)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[readme-renderer](https://github.com/pypa/readme_renderer) to permit the
latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/readme_renderer/releases">readme-renderer's
releases</a>.</em></p>
<blockquote>
<h2>44.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Support newer docutils versions by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/315">pypa/readme_renderer#315</a></li>
<li>Resolve Node 16 deprecation warnings in CI by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/309">pypa/readme_renderer#309</a></li>
<li>Lint specific directories by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/312">pypa/readme_renderer#312</a></li>
<li>Build a wheel once, for all test environments by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/308">pypa/readme_renderer#308</a></li>
<li>Update .gitpod.yml to replace deprecated extension by <a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/306">pypa/readme_renderer#306</a></li>
<li>Exclude .gitpod.yml by default with check-manifest by <a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/307">pypa/readme_renderer#307</a></li>
<li>Lazy open output files, and always close them by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/314">pypa/readme_renderer#314</a></li>
<li>Release 44 by <a
href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/316">pypa/readme_renderer#316</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a>
made their first contribution in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/315">pypa/readme_renderer#315</a></li>
<li><a
href="https://github.com/shenxianpeng"><code>@​shenxianpeng</code></a>
made their first contribution in <a
href="https://redirect.github.com/pypa/readme_renderer/pull/306">pypa/readme_renderer#306</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/pypa/readme_renderer/compare/43.0...44.0">https://github.com/pypa/readme_renderer/compare/43.0...44.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst">readme-renderer's
changelog</a>.</em></p>
<blockquote>
<h2>44.0 (2024-07-08)</h2>
<ul>
<li>Drop support for Python 3.8 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Require docutils 0.21.2 and higher (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Remove HTML5 <code>&lt;s&gt;</code> tag from the list of allowed
HTML tags (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Test all supported CPython and PyPy versions in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li>Resolve Node 16 deprecation warnings in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/309">#309</a>)</li>
<li>Lint specific directories (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/312">#312</a>)</li>
<li>Build a wheel once for all tox test environments (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/308">#308</a>)</li>
<li>Lazy open output files, and always close them (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/314">#314</a>)</li>
<li>Gitpod: Migrate to the Even Better TOML extension (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/306">#306</a>)</li>
<li>check-manifest: Remove a now-default <code>.gitpod.yml</code>
exclusion (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/307">#307</a>)</li>
</ul>
<h2>43.0 (2024-02-26)</h2>
<ul>
<li>Allow HTML5 <code>picture</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/299">#299</a>)</li>
<li>Test against Python 3.12 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/300">#300</a>)</li>
</ul>
<h2>42.0 (2023-09-07)</h2>
<ul>
<li>Migrate from <code>bleach</code> to <code>nh3</code> (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/295">#295</a>)</li>
<li>Migrate from <code>setup.py</code> to
<code>pyproject.toml</code></li>
</ul>
<h2>41.0 (2023-08-18)</h2>
<ul>
<li>Allow HTML5 <code>figcaption</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/291">#291</a>)</li>
<li>Test <code>README.rst</code> from this project (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/288">#288</a>)</li>
</ul>
<h2>40.0 (2023-06-16)</h2>
<ul>
<li>Add CLI option to render package README. (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/271">#271</a>)</li>
<li>Adapt tests to pygments 2.14.0 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/272">#272</a>)</li>
<li>Update release process to use Trusted Publishing (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/276">#276</a>)</li>
<li>Replace usage of deprecated <code>pkg_resources</code> with
<code>importlib.metadata</code> (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/281">#281</a>)</li>
<li>Drop support for Python 3.7 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/282">#282</a>),
Test against Python 3.11 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/280">#280</a>)</li>
</ul>
<h2>37.3 (2022-10-31)</h2>
<ul>
<li>Allow HTML5 <code>figure</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/265">#265</a>)</li>
</ul>
<h2>37.2 (2022-09-24)</h2>
<ul>
<li>Allow HTML5 <code>s</code> tag through cleaner (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/261">#261</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pypa/readme_renderer/commit/1d0497c37a6033d791c74e800590dcd0d34f6e08"><code>1d0497c</code></a>
Release 44 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/316">#316</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/09620a64219f80238e396b25ab18016ca495cf3c"><code>09620a6</code></a>
Lazy open output files, and always close them (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/314">#314</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/6061b3ebbcdecc33f369c0d48fe0641b34858294"><code>6061b3e</code></a>
Exclude .gitpod.yml by default with check-manifest (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/307">#307</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/749204b0eaa5a72fceb29c707d5321687ac447a3"><code>749204b</code></a>
Update .gitpod.yml to replace deprecated extension (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/306">#306</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/e84ded18e61e33ae117f20d0eefb1f92edc88ed0"><code>e84ded1</code></a>
Build a wheel once, for all test environments (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/308">#308</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/b447d5d7ba60ee71dff19f753d7b6c33312411b8"><code>b447d5d</code></a>
Lint specific directories (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/312">#312</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/08172046a88d019989c2de36f9cc0c88695cf2b2"><code>0817204</code></a>
Resolve Node 16 deprecation warnings in CI (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/309">#309</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/fefd2859fb3253744a21f327b2079cdd14240bfe"><code>fefd285</code></a>
Support newer docutils versions (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/315">#315</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/175c65ad514acad7fccf54c3aab9fe701bdd9f06"><code>175c65a</code></a>
Release 43.0 (<a
href="https://redirect.github.com/pypa/readme_renderer/issues/303">#303</a>)</li>
<li><a
href="https://github.com/pypa/readme_renderer/commit/78ccf3f50f58467e2b3886412d3ba8c7a3a398d4"><code>78ccf3f</code></a>
adds testing for 3.12, fixes <a
href="https://redirect.github.com/pypa/readme_renderer/issues/290">#290</a>
(<a
href="https://redirect.github.com/pypa/readme_renderer/issues/300">#300</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pypa/readme_renderer/compare/0.1.0...44.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:39:51 -04:00
dependabot[bot] 718b53cce2 Update markupsafe requirement from <2 to <4 (#539)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[markupsafe](https://github.com/pallets/markupsafe) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pallets/markupsafe/releases">markupsafe's
releases</a>.</em></p>
<blockquote>
<h2>3.0.2</h2>
<p>This is the MarkupSafe 3.0.2 fix release, which fixes bugs but does
not otherwise change behavior and should not result in breaking
changes.</p>
<p>PyPI: <a
href="https://pypi.org/project/MarkupSafe/3.0.2/">https://pypi.org/project/MarkupSafe/3.0.2/</a>
Changes: <a
href="https://markupsafe.palletsprojects.com/en/stable/changes/#version-3-0-2">https://markupsafe.palletsprojects.com/en/stable/changes/#version-3-0-2</a>
Milestone: <a
href="https://github.com/pallets/markupsafe/milestone/14?closed=1">https://github.com/pallets/markupsafe/milestone/14?closed=1</a></p>
<ul>
<li>Fix compatibility when <code>__str__</code> returns a
<code>str</code> subclass. <a
href="https://redirect.github.com/pallets/markupsafe/issues/472">#472</a></li>
<li>Build requires setuptools &gt;= 70.1. <a
href="https://redirect.github.com/pallets/markupsafe/issues/475">#475</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pallets/markupsafe/blob/main/CHANGES.rst">markupsafe's
changelog</a>.</em></p>
<blockquote>
<h2>Version 3.0.2</h2>
<p>Released 2024-10-18</p>
<ul>
<li>Fix compatibility when <code>__str__</code> returns a
<code>str</code> subclass. :issue:<code>472</code></li>
<li>Build requires setuptools &gt;= 70.1. :issue:<code>475</code></li>
</ul>
<h2>Version 3.0.1</h2>
<p>Released 2024-10-08</p>
<ul>
<li>Address compiler warnings that became errors in GCC 14.
:issue:<code>466</code></li>
<li>Fix compatibility with proxy objects. :issue:<code>467</code></li>
</ul>
<h2>Version 3.0.0</h2>
<p>Released 2024-10-07</p>
<ul>
<li>Support Python 3.13 and its experimental free-threaded build.
:pr:<code>461</code></li>
<li>Drop support for Python 3.7 and 3.8.</li>
<li>Use modern packaging metadata with <code>pyproject.toml</code>
instead of <code>setup.cfg</code>.
:pr:<code>348</code></li>
<li>Change <code>distutils</code> imports to <code>setuptools</code>.
:pr:<code>399</code></li>
<li>Use deferred evaluation of annotations. :pr:<code>400</code></li>
<li>Update signatures for <code>Markup</code> methods to match
<code>str</code> signatures. Use
positional-only arguments. :pr:<code>400</code></li>
<li>Some <code>str</code> methods on <code>Markup</code> no longer
escape their argument:
<code>strip</code>, <code>lstrip</code>, <code>rstrip</code>,
<code>removeprefix</code>, <code>removesuffix</code>,
<code>partition</code>, and <code>rpartition</code>;
<code>replace</code> only escapes its <code>new</code>
argument. These methods are conceptually linked to search methods such
as
<code>in</code>, <code>find</code>, and <code>index</code>, which
already do not escape their argument.
:issue:<code>401</code></li>
<li>The <code>__version__</code> attribute is deprecated. Use feature
detection, or
<code>importlib.metadata.version(&quot;markupsafe&quot;)</code>,
instead. :pr:<code>402</code></li>
<li>Speed up escaping plain strings by 40%. :pr:<code>434</code></li>
<li>Simplify speedups implementation. :pr:<code>437</code></li>
</ul>
<h2>Version 2.1.5</h2>
<p>Released 2024-02-02</p>
<ul>
<li>Fix <code>striptags</code> not collapsing spaces.
:issue:<code>417</code></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pallets/markupsafe/commit/28ace20b140d15c083e1cbc163ee6b7778ba098c"><code>28ace20</code></a>
release version 3.0.2</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/6b51fd8f7386983b7038ad973557367cbd48579a"><code>6b51fd8</code></a>
build requires at least setuptools 70.1 (<a
href="https://redirect.github.com/pallets/markupsafe/issues/478">#478</a>)</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/99dda9fd708432bd07d02327b2668661aa3cdaa0"><code>99dda9f</code></a>
build requires at least setuptools 70.1</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/3d8fd8cc006124a49ce2f4268b4d1739e301583e"><code>3d8fd8c</code></a>
fix version</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/1933c4be9c2c88613f7660840cde27a1bb7567e0"><code>1933c4b</code></a>
fix version</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/e85aff4d878aa458d5c1e879bf475d8483647f71"><code>e85aff4</code></a>
relax speedups str check (<a
href="https://redirect.github.com/pallets/markupsafe/issues/477">#477</a>)</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/8cb1691ca038ca39942e088b956f5b94d8f636bf"><code>8cb1691</code></a>
relax speedups str check</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/4dafb7c36f1f654f1edd85228d346252b0065d45"><code>4dafb7c</code></a>
start version 3.1.0</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/9c44ecf45141f691d373a66ce664c43b5a6cc761"><code>9c44ecf</code></a>
update docs build</li>
<li><a
href="https://github.com/pallets/markupsafe/commit/275c76905617c3f0e34de14e8794fcf4dfb0f937"><code>275c769</code></a>
Merge branch '2.1.x' into 3.0.x</li>
<li>Additional commits viewable in <a
href="https://github.com/pallets/markupsafe/compare/0.9...3.0.2">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:39:11 -04:00
dependabot[bot] 2e0b4975f7 Update sphinxcontrib-websupport requirement from <1.2 to <2.1 (#538)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updates the requirements on
[sphinxcontrib-websupport](https://github.com/sphinx-doc/sphinxcontrib-websupport)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/releases">sphinxcontrib-websupport's
releases</a>.</em></p>
<blockquote>
<h2>sphinxcontrib-websupport 2.0.0</h2>
<p>Changelog: <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst">https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/blob/master/CHANGES.rst">sphinxcontrib-websupport's
changelog</a>.</em></p>
<blockquote>
<h1>Release 2.0.0 (2024-07-28)</h1>
<ul>
<li>Adopt Ruff</li>
<li>Tighten MyPy settings</li>
<li>Update GitHub actions versions</li>
</ul>
<h1>Release 1.2.7 (2024-01-13)</h1>
<ul>
<li>Fix tests for sqlalchemy 2.</li>
<li>Publish a <code>whoosh</code> extra.</li>
</ul>
<h1>Release 1.2.6 (2023-08-09)</h1>
<ul>
<li>Fix tests for Sphinx 7.1 and below</li>
</ul>
<h1>Release 1.2.5 (2023-08-07)</h1>
<ul>
<li>Drop support for Python 3.5, 3.6, 3.7, and 3.8</li>
<li>Raise minimum required Sphinx version to 5.0</li>
</ul>
<h1>Release 1.2.4 (2020-08-09)</h1>
<ul>
<li>Import PickleHTMLBuilder from sphinxcontrib-serializinghtml
package</li>
</ul>
<h1>Release 1.2.3 (2020-06-27)</h1>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinxcontrib-websupport/issues/43">#43</a>:
doctreedir argument has been ignored on initialize app</li>
</ul>
<h1>Release 1.2.2 (2020-04-29)</h1>
<ul>
<li>Stop to use sphinx.util.pycompat:htmlescape</li>
</ul>
<h1>Release 1.2.1 (2020-03-21)</h1>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinxcontrib-websupport/issues/41">#41</a>:
templates/searchresults.html is missing in the source tarball</li>
</ul>
<h1>Release 1.2.0 (2020-02-07)</h1>
<ul>
<li>Drop python2.7 and 3.4 support</li>
</ul>
<p>Release 1.1.2 (2019-05-19)</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/142b41e404e0197c8b48147284302cb6aa8b4207"><code>142b41e</code></a>
Bump to 2.0.0</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/6a625bd314a7338c3a91ebdf846743421387092d"><code>6a625bd</code></a>
Update CHANGES links</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/b6906da79bfff09120d43a0aa551b89d774ea3af"><code>b6906da</code></a>
Rename LICENSE to LICENCE.rst</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/a21e38577951121bd92f5a5606a012dc75a0a32b"><code>a21e385</code></a>
Rename CHANGES to CHANGES.rst</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/992d6fd2fd4ed1185d424596517a0c81be6c039b"><code>992d6fd</code></a>
Run CI with Python 3.12 releases</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/6c0277eb35aa2866b18d9bdc111fc074719309f0"><code>6c0277e</code></a>
Run mypy without command-line options</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/83f178dcc1e446956d8a24de06afe8222dc48e1b"><code>83f178d</code></a>
Use the latest GitHub actions versions</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/155ae9ca9f26c022d79f93058da75b7385e1adf1"><code>155ae9c</code></a>
Enable GitHub's dependabot package update service</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/7f05400e51a9bae54009f90cc60dbee83341c087"><code>7f05400</code></a>
Adopt Ruff and use stricter MyPy settings</li>
<li><a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/commit/be777a7ed355ac85234646505c6ce402966d7543"><code>be777a7</code></a>
Update .gitignore</li>
<li>Additional commits viewable in <a
href="https://github.com/sphinx-doc/sphinxcontrib-websupport/compare/1.0.0...2.0.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2024-10-24 07:38:58 -04:00
dependabot[bot] a118a5dc4b Bump actions/setup-python from 4 to 5 (#537)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, we update node version runtime from node16
to node20 (<a
href="https://redirect.github.com/actions/setup-python/pull/772">actions/setup-python#772</a>).
Besides, we update dependencies to the latest versions.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0">https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0</a></p>
<h2>v4.8.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release we added support for GraalPy (<a
href="https://redirect.github.com/actions/setup-python/pull/694">actions/setup-python#694</a>).
You can use this snippet to set up GraalPy:</p>
<pre lang="yaml"><code>steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4 
  with:
    python-version: 'graalpy-22.3' 
- run: python my_script.py
</code></pre>
<p>Besides, the release contains such changes as:</p>
<ul>
<li>Trim python version when reading from file by <a
href="https://github.com/FerranPares"><code>@​FerranPares</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/628">actions/setup-python#628</a></li>
<li>Use non-deprecated versions in examples by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/724">actions/setup-python#724</a></li>
<li>Change deprecation comment to past tense by <a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/723">actions/setup-python#723</a></li>
<li>Bump <code>@​babel/traverse</code> from 7.9.0 to 7.23.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/743">actions/setup-python#743</a></li>
<li>advanced-usage.md: Encourage the use actions/checkout@v4 by <a
href="https://github.com/cclauss"><code>@​cclauss</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/729">actions/setup-python#729</a></li>
<li>Examples now use checkout@v4 by <a
href="https://github.com/simonw"><code>@​simonw</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/738">actions/setup-python#738</a></li>
<li>Update actions/checkout to v4 by <a
href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/761">actions/setup-python#761</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/FerranPares"><code>@​FerranPares</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/628">actions/setup-python#628</a></li>
<li><a href="https://github.com/timfel"><code>@​timfel</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/694">actions/setup-python#694</a></li>
<li><a
href="https://github.com/jeffwidman"><code>@​jeffwidman</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/724">actions/setup-python#724</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4...v4.8.0">https://github.com/actions/setup-python/compare/v4...v4.8.0</a></p>
<h2>v4.7.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump word-wrap from 1.2.3 to 1.2.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/702">actions/setup-python#702</a></li>
<li>Add range validation for toml files by <a
href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/726">actions/setup-python#726</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v4...v4.7.1">https://github.com/actions/setup-python/compare/v4...v4.7.1</a></p>
<h2>v4.7.0</h2>
<p>In scope of this release, the support for reading python version from
pyproject.toml was added (<a
href="https://redirect.github.com/actions/setup-python/pull/669">actions/setup-python#669</a>).</p>
<pre lang="yaml"><code>      - name: Setup Python
        uses: actions/setup-python@v4
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-python/commit/f677139bbe7f9c59b41e40162b753c062f5d49a3"><code>f677139</code></a>
Bump pyinstaller from 3.6 to 5.13.1 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/923">#923</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/2bd53f9a4d1dd1cd21eaffcc01a7b91a8e73ea4c"><code>2bd53f9</code></a>
Documentation update for caching poetry dependencies (<a
href="https://redirect.github.com/actions/setup-python/issues/908">#908</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/80b49d3ed89312896dbdcbefc2ddb159c7f8ca43"><code>80b49d3</code></a>
fix: add arch to cache key (<a
href="https://redirect.github.com/actions/setup-python/issues/896">#896</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/036a5236741fd24c89eea80d1b76179e8e5f9214"><code>036a523</code></a>
Fix: Add <code>.zip</code> extension to Windows package downloads for
<code>Expand-Archive</code> C...</li>
<li><a
href="https://github.com/actions/setup-python/commit/04c1311429f7be71707d8ab66c7af8a14e54b938"><code>04c1311</code></a>
Fix display of emojis in contributors doc (<a
href="https://redirect.github.com/actions/setup-python/issues/899">#899</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/cb6845644151e35f879e10f2f0896c3c8bee372c"><code>cb68456</code></a>
Updated <code>@​iarna/toml</code> version to 3.0.0 (<a
href="https://redirect.github.com/actions/setup-python/issues/912">#912</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/39cd14951b08e74b54015e9e001cdefcf80e669f"><code>39cd149</code></a>
Documentation update for cache (<a
href="https://redirect.github.com/actions/setup-python/issues/873">#873</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/a0d74c0c423f896bc4e7be91d5cb1e2d54438db3"><code>a0d74c0</code></a>
fix(ci): update all failing workflows (<a
href="https://redirect.github.com/actions/setup-python/issues/863">#863</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/4eb7dbcb9561cb76a85079ffa9d89b983166e00c"><code>4eb7dbc</code></a>
Bump braces from 3.0.2 to 3.0.3 (<a
href="https://redirect.github.com/actions/setup-python/issues/893">#893</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/82c7e631bb3cdc910f68e0081d67478d79c6982d"><code>82c7e63</code></a>
Documentation changes for avoiding rate limit issues on GHES (<a
href="https://redirect.github.com/actions/setup-python/issues/835">#835</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-python/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
2024-10-24 07:38:37 -04:00
dependabot[bot] 69c1d7f185 Bump actions/checkout from 3 to 4 (#536)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to
4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update default runtime to node20 by <a
href="https://github.com/takost"><code>@​takost</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1436">actions/checkout#1436</a></li>
<li>Support fetching without the --progress option by <a
href="https://github.com/simonbaird"><code>@​simonbaird</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1067">actions/checkout#1067</a></li>
<li>Release 4.0.0 by <a
href="https://github.com/takost"><code>@​takost</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1447">actions/checkout#1447</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/takost"><code>@​takost</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1436">actions/checkout#1436</a></li>
<li><a
href="https://github.com/simonbaird"><code>@​simonbaird</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1067">actions/checkout#1067</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3...v4.0.0">https://github.com/actions/checkout/compare/v3...v4.0.0</a></p>
<h2>v3.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Mark test scripts with Bash'isms to be run via Bash by <a
href="https://github.com/dscho"><code>@​dscho</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1377">actions/checkout#1377</a></li>
<li>Add option to fetch tags even if fetch-depth &gt; 0 by <a
href="https://github.com/RobertWieczoreck"><code>@​RobertWieczoreck</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/579">actions/checkout#579</a></li>
<li>Release 3.6.0 by <a
href="https://github.com/luketomlinson"><code>@​luketomlinson</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1437">actions/checkout#1437</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/RobertWieczoreck"><code>@​RobertWieczoreck</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/579">actions/checkout#579</a></li>
<li><a
href="https://github.com/luketomlinson"><code>@​luketomlinson</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1437">actions/checkout#1437</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3.5.3...v3.6.0">https://github.com/actions/checkout/compare/v3.5.3...v3.6.0</a></p>
<h2>v3.5.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Checkout Issue in self hosted runner due to faulty submodule
check-ins by <a
href="https://github.com/megamanics"><code>@​megamanics</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1196">actions/checkout#1196</a></li>
<li>Fix typos found by codespell by <a
href="https://github.com/DimitriPapadopoulos"><code>@​DimitriPapadopoulos</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1287">actions/checkout#1287</a></li>
<li>Add support for sparse checkouts by <a
href="https://github.com/dscho"><code>@​dscho</code></a> and <a
href="https://github.com/dfdez"><code>@​dfdez</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1369">actions/checkout#1369</a></li>
<li>Release v3.5.3 by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1376">actions/checkout#1376</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/megamanics"><code>@​megamanics</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1196">actions/checkout#1196</a></li>
<li><a
href="https://github.com/DimitriPapadopoulos"><code>@​DimitriPapadopoulos</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1287">actions/checkout#1287</a></li>
<li><a href="https://github.com/dfdez"><code>@​dfdez</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1369">actions/checkout#1369</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3...v3.5.3">https://github.com/actions/checkout/compare/v3...v3.5.3</a></p>
<h2>v3.5.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Use correct API url / endpoint in GHES by <a
href="https://github.com/fhammerl"><code>@​fhammerl</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1289">actions/checkout#1289</a>
based on <a
href="https://redirect.github.com/actions/checkout/issues/1286">#1286</a>
by <a href="https://github.com/1newsr"><code>@​1newsr</code></a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v3.5.1...v3.5.2">https://github.com/actions/checkout/compare/v3.5.1...v3.5.2</a></p>
<h2>v3.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Improve checkout performance on Windows runners by upgrading
<code>@​actions/github</code> dependency by <a
href="https://github.com/BrettDong"><code>@​BrettDong</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1246">actions/checkout#1246</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/BrettDong"><code>@​BrettDong</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1246">actions/checkout#1246</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<ul>
<li>Check git version before attempting to disable
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1656">actions/checkout#1656</a></li>
<li>Add SSH user parameter by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1685">actions/checkout#1685</a></li>
<li>Update <code>actions/checkout</code> version in
<code>update-main-version.yml</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1650">actions/checkout#1650</a></li>
</ul>
<h2>v4.1.2</h2>
<ul>
<li>Fix: Disable sparse checkout whenever <code>sparse-checkout</code>
option is not present <a
href="https://github.com/dscho"><code>@​dscho</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1598">actions/checkout#1598</a></li>
</ul>
<h2>v4.1.1</h2>
<ul>
<li>Correct link to GitHub Docs by <a
href="https://github.com/peterbe"><code>@​peterbe</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1511">actions/checkout#1511</a></li>
<li>Link to release page from what's new section by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1514">actions/checkout#1514</a></li>
</ul>
<h2>v4.1.0</h2>
<ul>
<li><a href="https://redirect.github.com/actions/checkout/pull/1396">Add
support for partial checkout filters</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/11bd71901bbe5b1630ceea73d27597364c9af683"><code>11bd719</code></a>
Prepare 4.2.2 Release (<a
href="https://redirect.github.com/actions/checkout/issues/1953">#1953</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e3d2460bbb42d7710191569f88069044cfb9d8cf"><code>e3d2460</code></a>
Expand unit test coverage (<a
href="https://redirect.github.com/actions/checkout/issues/1946">#1946</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/163217dfcd28294438ea1c1c149cfaf66eec283e"><code>163217d</code></a>
<code>url-helper.ts</code> now leverages well-known environment
variables. (<a
href="https://redirect.github.com/actions/checkout/issues/1941">#1941</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871"><code>eef6144</code></a>
Prepare 4.2.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/1925">#1925</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/6b42224f41ee5dfe5395e27c8b2746f1f9955030"><code>6b42224</code></a>
Add workflow file for publishing releases to immutable action package
(<a
href="https://redirect.github.com/actions/checkout/issues/1919">#1919</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/de5a000abf73b6f4965bd1bcdf8f8d94a56ea815"><code>de5a000</code></a>
Check out other refs/* by commit if provided, fall back to ref (<a
href="https://redirect.github.com/actions/checkout/issues/1924">#1924</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d632683dd7b4114ad314bca15554477dd762a938"><code>d632683</code></a>
Prepare 4.2.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/1878">#1878</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/6d193bf28034eafb982f37bd894289fe649468fc"><code>6d193bf</code></a>
Bump braces from 3.0.2 to 3.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/1777">#1777</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/db0cee9a514becbbd4a101a5fbbbf47865ee316c"><code>db0cee9</code></a>
Bump the minor-npm-dependencies group across 1 directory with 4 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/1872">#1872</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b6849436894e144dbce29d7d7fda2ae3bf9d8365"><code>b684943</code></a>
Add Ref and Commit outputs (<a
href="https://redirect.github.com/actions/checkout/issues/1180">#1180</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:38:03 -04:00
dependabot[bot] fba2f135a3 Update sphinx requirement from <6,>=5 to >=5,<9 (#542)
Updates the requirements on
[sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinx/releases">sphinx's
releases</a>.</em></p>
<blockquote>
<h2>Sphinx 8.1.3</h2>
<p>Changelog: <a
href="https://www.sphinx-doc.org/en/master/changes/8.1.html">https://www.sphinx-doc.org/en/master/changes/8.1.html</a></p>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13013">#13013</a>:
Restore support for <code>cut_lines()</code> with no object type. Patch
by Adam Turner.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sphinx-doc/sphinx/blob/v8.1.3/CHANGES.rst">sphinx's
changelog</a>.</em></p>
<blockquote>
<h1>Release 8.1.3 (released Oct 13, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13013">#13013</a>:
Restore support for :func:<code>!cut_lines</code> with no object type.
Patch by Adam Turner.</li>
</ul>
<h1>Release 8.1.2 (released Oct 12, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13012">#13012</a>:
Expose :exc:<code>sphinx.errors.ExtensionError</code> in
<code>sphinx.util</code>
for backwards compatibility.
This will be removed in Sphinx 9, as exposing the exception
in <code>sphinx.util</code> was never intentional.
:exc:<code>!ExtensionError</code> has been part of
<code>sphinx.errors</code> since Sphinx 0.9.
Patch by Adam Turner.</li>
</ul>
<h1>Release 8.1.1 (released Oct 11, 2024)</h1>
<h2>Bugs fixed</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13006">#13006</a>:
Use the preferred <a
href="https://www.cve.org/">https://www.cve.org/</a> URL for
the :rst:role:<code>:cve: &lt;cve&gt;</code> role.
Patch by Hugo van Kemenade.</li>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13007">#13007</a>:
LaTeX: Improve resiliency when the required
<code>fontawesome</code> or <code>fontawesome5</code> packages are not
installed.
Patch by Jean-François B.</li>
</ul>
<h1>Release 8.1.0 (released Oct 10, 2024)</h1>
<h2>Dependencies</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12756">#12756</a>:
Add lower-bounds to the <code>sphinxcontrib-*</code> dependencies.
Patch by Adam Turner.</li>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12833">#12833</a>:
Update the LaTeX <code>parskip</code> package from 2001 to 2018.
Patch by Jean-François B.</li>
</ul>
<h2>Incompatible changes</h2>
<ul>
<li><a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12763">#12763</a>:
Remove unused internal class <code>sphinx.util.Tee</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/a1510de4777eaa2e569435f95b05f6f3293d7035"><code>a1510de</code></a>
Bump to 8.1.3 final</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/62e9606d63c8bbb4964213fd6b427d1483847662"><code>62e9606</code></a>
Restore support for <code>cut_lines()</code> with no object type (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13015">#13015</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/5ae32ce9bfe4a17a7f00e1e8d39a80449423c726"><code>5ae32ce</code></a>
Bump version</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/a72b47bb408923cb7809eb9f96885545184e3773"><code>a72b47b</code></a>
Bump to 8.1.2 final</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/39a45ad4073a4d8c3b7dfd64d22e8a88870dcc7c"><code>39a45ad</code></a>
Expose <code>ExtensionError</code> in <code>sphinx.util</code> for
backwards compatibility.</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/5a4859a2e489c66b38804e95bf77fd0baf4320dc"><code>5a4859a</code></a>
Add docs about sphinx-autobuild (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13011">#13011</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/05679efe7b34f8b2fb87605438c40248ac8cae83"><code>05679ef</code></a>
Type-check the 'autodoc_intenum' example (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/12827">#12827</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/86d1d31fb370f031739079de7d827be0074e7661"><code>86d1d31</code></a>
Prune CHANGES of unneeded sections</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/b6269d3790bb3bdd652ce67fecb59e6afddc8014"><code>b6269d3</code></a>
Improve documentation for the Builder API (<a
href="https://redirect.github.com/sphinx-doc/sphinx/issues/13008">#13008</a>)</li>
<li><a
href="https://github.com/sphinx-doc/sphinx/commit/c46abc47210088a6c4fee9dac23badfcebc441d7"><code>c46abc4</code></a>
Improve clarity for <code>master_doc</code> and
<code>root_doc</code></li>
<li>Additional commits viewable in <a
href="https://github.com/sphinx-doc/sphinx/compare/v5.0.0...v8.1.3">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 07:37:38 -04:00
Andreas Motl 4006de72cd Dependencies: Add Dependabot configuration (#534)
What the title says.
2024-10-24 07:30:41 -04:00
Andreas Motl b3c7252197 Chore: Format code using Ruff, and fix linter errors (#531)
## About
- Add Ruff configuration to `pyproject.toml`, apply its formatter, and
satisfy its linter.
- Migrate pytest configuration to `pyproject.toml`.
2024-10-24 07:30:18 -04:00
Andreas Motl 398ac3343e This and that: 20241024-02 (#530)
## About
After GH-529, another round of improvements submitted as a bundle.

## References
- GH-529
2024-10-23 21:04:03 -04:00
Andreas Motl 8b197ba361 CI: Improve test matrix configuration. Add macOS, both Intel and ARM. 2024-10-24 02:27:32 +02:00
Andreas Motl e700aa2937 Chore: Update LICENSE file
GitHub wasn't able to discover the license (badge) from the LICENSE
file. Let's use a vanilla variant for Apache 2.0.
2024-10-24 02:27:32 +02:00
Andreas Motl 3894550642 Tests: Enable pytest options, increasing verbosity
That is to display test case names, not just dots that don't convey
much.
2024-10-24 02:27:32 +02:00
Andreas Motl 43fd041138 CI: Add PyPy to Python test matrix 2024-10-24 02:27:32 +02:00
Andreas Motl 363af5338d CI: Properly verify package on Python 3.10, 3.11, and 3.12 2024-10-24 02:27:32 +02:00
Andreas Motl 55430a4366 Dependencies: Separate runtime vs. test vs. development definitions 2024-10-24 02:27:32 +02:00
kennethreitz f7c6a3ae97 Docs: Update to jinja2<3.2, Dependabot admonitions versions <3.1.4 (#528)
## About
What the title says.

## Details
It's only about building static docs, so there is no danger for this
project. It's just a chore fix to properly dismiss the security warning
signalled by Dependabot.

## References
- https://github.com/kennethreitz/responder/security/dependabot/61
2024-10-23 19:38:02 -04:00
Andreas Motl dcadba1425 Docs: Update to jinja2<3.2, Dependabot admonitions versions <3.1.4 2024-10-24 01:36:07 +02:00
kennethreitz de08b15ae8 Docs: Minimally modernize Sphinx configuration. Fix building on Python 3.11. (#526)
## About
Just a little maintenance patch for the Sphinx docs, to minimally
modernize dependencies, and to fix the build [^1].

[^1]: [...] and to probe if any commit styles of mine (commit messages,
wording, whatever) need to be adjusted to comply with any policies
employed here.
2024-10-23 19:32:44 -04:00
kennethreitz 0cfca6d906 Merge branch 'main' into docs-dependencies 2024-10-23 19:30:17 -04:00
kennethreitz a73e413a66 CI: Slightly update GHA configuration, now targeting branch main (#527)
## Problem
CI did not start on GH-526.

## Details
Also, add a configuration snippet to cancel redundant in-progress jobs,
in order to save resources. That means running jobs are terminated when
subsequently pushing to the same branch / updating the same PR,
DWIM-like.
2024-10-23 19:30:01 -04:00
Andreas Motl 87931a25d0 CI: Slightly update GHA configuration, now targeting branch main
Also, add a configuration snippet to cancel redundant in-progress jobs.
That means running jobs are terminated when subsequently pushing to the
same branch, in order to save resources.
2024-10-24 01:24:57 +02:00
Andreas Motl 1fd9a682dd Docs: Fix broken links 2024-10-24 01:17:54 +02:00
Andreas Motl 5d3e650901 Docs: Update dependencies, fixing the build on Python 3.11 2024-10-24 01:13:22 +02:00
Andreas Motl 48d082e6a5 Docs: Use relaxed upper-bound dependency pinning for Sphinx dependencies 2024-10-24 01:04:38 +02:00
Andreas Motl 87e22481e8 Docs: Clean up docs/requirements.txt. It just needs Sphinx and friends. 2024-10-24 01:04:08 +02:00
Andreas Motl e48ce6c301 Chore: Update .gitignore to ignore all virtualenvs 2024-10-24 01:02:25 +02:00
kennethreitz e9613500da Delete Pipfile (#516) 2024-03-31 10:56:22 -04:00
kennethreitz c2943accd0 Delete Pipfile 2024-03-31 10:54:49 -04:00
kennethreitz 649a255657 remove files 2024-03-30 20:43:28 -04:00
kennethreitz 7eaaaaafe1 Add Flask to requirements.txt 2024-03-30 20:37:52 -04:00
kennethreitz ae09b88978 Add typesystem==0.2.5 to requirements.txt 2024-03-30 20:37:05 -04:00
kennethreitz e3e307fd68 Update uv pip install command to use --system flag 2024-03-30 20:36:10 -04:00
kennethreitz 89f0724029 Update test.yaml workflow 2024-03-30 20:31:20 -04:00
kennethreitz bebe62adaf Add apistar to requirements.txt 2024-03-30 20:28:08 -04:00
kennethreitz eb9cddc8c2 Update dependency installation command 2024-03-30 20:27:30 -04:00
kennethreitz 7c19eca78a Remove unused code and dependencies 2024-03-30 20:26:43 -04:00
kennethreitz ed28b11d21 remove schema_doc.py 2024-03-30 20:21:38 -04:00
kennethreitz 46cdd4a245 Update GraphQL dependencies 2024-03-30 20:18:42 -04:00
kennethreitz ac91b172e6 Add graphql_server to requirements.txt 2024-03-30 20:15:50 -04:00
kennethreitz ed0da6d462 test 2024-03-30 20:14:26 -04:00
kennethreitz 555e9bff65 Add helloworld.py and update serve method in api.py 2024-03-30 20:11:42 -04:00
kennethreitz bf43d9f202 Add graphene to required packages 2024-03-30 20:08:25 -04:00
kennethreitz e239cc304d Update Python versions in test.yaml 2024-03-30 20:06:05 -04:00
kennethreitz 3285bd57c7 Update python_requires to >=3.11 2024-03-30 20:05:53 -04:00
kennethreitz 3090fb9e68 Update branch name in GitHub Actions workflow 2024-03-30 20:03:52 -04:00
kennethreitz e90bd24ebe Update test.yaml, add Pipfile, and delete httpbin.py 2024-03-30 20:02:39 -04:00
kennethreitz a0acc03a97 delete lint 2024-03-30 19:56:14 -04:00
kennethreitz 8a668e6efe Update GitHub Actions workflow for Python testing 2024-03-30 19:55:56 -04:00
kennethreitz 4c75742e4d Update Python versions and operating systems in test.yaml 2024-03-30 19:54:18 -04:00
kennethreitz 796fdc2ddf Fix commented out code in test_responder.py 2024-03-30 19:48:57 -04:00
kennethreitz a8caa3054b Update API requests from GET to POST 2024-03-30 19:44:45 -04:00
kennethreitz 2ef9e133ad Remove unused dependencies and update setup.py 2024-03-30 19:37:46 -04:00
kennethreitz 2ec570ad61 Refactor code by removing unused imports and properties 2024-03-30 19:35:12 -04:00
taoufik07 02aa338970 v2.0.7 2021-01-08 09:27:53 +01:00
Taoufik 882250bd86 Merge pull request #450 from taoufik07/uvicron_extra_standard
Add uvicorn[extra]
2021-01-08 09:26:31 +01:00
taoufik07 3809eda2f2 Use uvicorn[standard] 2021-01-07 21:39:17 +01:00
Taoufik b32eda70d2 Merge pull request #448 from taoufik07/taoufik07-patch-1
Add test for 3.9
2021-01-06 09:05:23 +01:00
taoufik07 f1b2f46a10 v2.0.6 2021-01-06 09:02:42 +01:00
Taoufik cf82dac4ad Add test for 3.9 2021-01-06 08:58:58 +01:00
Taoufik a0913e3f63 Merge pull request #447 from timgates42/bugfix_typo_marshmallow
docs: fix simple typo, mashmallow -> marshmallow
2020-12-24 08:44:43 +01:00
Tim Gates f90955a9b9 docs: fix simple typo, mashmallow -> marshmallow
There is a small typo in responder/ext/schema/__init__.py.

Should read `marshmallow` rather than `mashmallow`.
2020-12-24 13:27:01 +11:00
Taoufik 3736c9229d Merge pull request #429 from taoufik07/dependabot/pip/docs/bleach-3.1.4
Bump bleach from 3.1.1 to 3.1.4 in /docs
2020-12-02 09:44:55 +01:00
dependabot[bot] a802853367 Bump bleach from 3.1.1 to 3.1.4 in /docs
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.1 to 3.1.4.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.1...v3.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-01 23:11:49 +00:00
Taoufik 96ca88fe88 Merge pull request #446 from taoufik07/github_actions
Switch to github actions
2020-12-02 00:11:05 +01:00
taoufik07 a57570210a Add prettier to pre-commit 2020-12-01 23:36:42 +01:00
taoufik07 7682e94b35 Disable tests in CI for windows 2020-12-01 23:10:24 +01:00
taoufik07 8bbebe113c Bump black 2020-12-01 23:10:24 +01:00
taoufik07 7c921f827b Switch to github actions 2020-12-01 23:10:24 +01:00
taoufik07 4cc055f93a Add pre-commit 2020-12-01 23:10:21 +01:00
Taoufik e596a8b457 Merge pull request #444 from majiang/patch-1
Call user-provided `default_response`
2020-12-01 21:42:13 +01:00
Taoufik fd2da55880 Merge pull request #445 from majiang/patch-2
test_redirects: access '/2' and redirect to '/1'
2020-12-01 21:37:28 +01:00
majiang 975e9b5643 test_redirects: access '/2' and redirect to '/1' 2020-12-01 16:18:56 +09:00
majiang c0036e0474 Call user-provided default_response
I'm not 100% sure, but it seems that user-provided `default_response`, stored as `Router.default_endpoint`, should be called when no match was found.
2020-12-01 16:15:57 +09:00
Taoufik 103816e27a Merge pull request #439 from ryuuji/master
bump uvicorn 0.11.* to 0.11.7
2020-08-12 11:01:13 +02:00
Ryuuji Yoshimoto b7c1684ab4 bump 0.11.7 2020-08-11 18:57:35 +09:00
Ryuuji Yoshimoto 16bd6ca266 bump uvicorn 2020-08-11 18:53:59 +09:00
Taoufik 20bae4712b Merge pull request #430 from ucpr/fix-lock
Fix hash in pygment from piwheel to pypi.
2020-04-17 05:40:29 +02:00
ucpr a7aa80c690 Fix hash in pygment from piwheel to pypi. 2020-04-15 00:30:34 +09:00
Taoufik df89d1d58b Merge pull request #424 from taoufik07/starlette_0_13
Fixes and bump dependencies
2020-03-09 05:32:30 +01:00
taoufik07 477cddd29c travisci add python 3.8 2020-03-09 05:26:48 +01:00
taoufik07 9b8cf3a1b1 Bump uvicorn version to 0.11 2020-03-09 05:16:34 +01:00
taoufik07 2871a3c07f starlette 0.13 and fix lifespan 2020-03-09 05:13:12 +01:00
Taoufik 13763296dd Merge pull request #421 from taoufik07/dependabot/pip/docs/bleach-3.1.1
Bump bleach from 3.0.2 to 3.1.1 in /docs
2020-02-25 02:24:11 +01:00
dependabot[bot] 783b22ab1c Bump bleach from 3.0.2 to 3.1.1 in /docs
Bumps [bleach](https://github.com/mozilla/bleach) from 3.0.2 to 3.1.1.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.0.2...v3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-24 18:31:05 +00:00
Taoufik 109937adf4 Merge pull request #414 from taoufik07/v2.0.5
v2.0.5
2019-12-15 16:38:02 +01:00
taoufik 63ea9cc4e0 v2.0.5 2019-12-15 16:32:47 +01:00
Taoufik ec40a0c4c3 Merge pull request #413 from taoufik07/support_python3.8
Update requirements to support python3.8
2019-12-15 16:28:17 +01:00
taoufik 0855d1a378 Update requirements to support python3.8 2019-12-15 16:22:26 +01:00
Taoufik 77fe17d350 Merge pull request #411 from StevenAvelino24/feature/use-openapi-params
Use OpenAPI info params on init of API
2019-12-04 20:07:46 +01:00
Steven Avelino 0b8a031ccb Use openapi info params on init of API 2019-12-04 14:50:30 +01:00
taoufik 0678daa880 Bump to v2.0.4 2019-11-19 17:35:16 +01:00
Taoufik 6761e3bdd8 Merge pull request #406 from daphil19/master
fix issues related to using `static=true` in `api.add_route()` and `static_route` in `responder.API()`
2019-11-19 17:30:22 +01:00
David Phillips ead213a506 responder now checks routes added via add_route before mounted routes
routes added by `add_route` (including @api.route()) are now checked
before routes that were added by api.mount()

this especially helps cases for situations where the static route
defined by the API class is '/' but the default route is '/' as well
2019-11-02 13:57:39 -04:00
David Phillips 75b5782eee fix static_response endpoint to return index.html contents 2019-11-02 13:55:34 -04:00
Taoufik a80df809e4 Merge pull request #399 from vbarbaresi/fix_render_async
Async templates require enable_async in jinja2 Environment
2019-10-25 22:19:04 +01:00
taoufik 7f3177f662 v2.0.3 2019-10-22 10:41:34 +01:00
Vincent Barbaresi 86361523e2 fix async templates rendering requiring enable_async in jinja2
When trying to test the render_async() feature I realized that it wasn't working
The template instance needs to be created with enable_async=True
2019-10-19 14:49:50 +02:00
110 changed files with 8071 additions and 2150 deletions
+3
View File
@@ -0,0 +1,3 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
+16
View File
@@ -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"
+54
View File
@@ -0,0 +1,54 @@
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: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python-version: ["3.13"]
env:
UV_SYSTEM_PYTHON: true
steps:
- uses: actions/checkout@v4
- 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 and documentation dependencies
run: |
uv pip install '.[develop,docs]'
- name: Run link checker
run: |
poe docs-linkcheck
- name: Build static HTML documentation
run: |
poe docs-html
+55
View File
@@ -0,0 +1,55 @@
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 and validate package
run: |
uv pip install '.[develop,test]'
poe check
+2
View File
@@ -1,3 +1,4 @@
.venv*
.vscode/
.cache
.idea
@@ -6,6 +7,7 @@
.pytest_cache
.DS_Store
coverage.xml
.coverage*
__pycache__
tests/__pycache__
+33
View File
@@ -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
View File
@@ -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
+203 -46
View File
@@ -1,265 +1,422 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [v2.0.2] - 2019-09-20
## [v3.0.0] - 2026-03-22
### Added
- Platform: Added support for Python 3.10 - Python 3.13
- CLI: `responder run` now also accepts a filesystem path on its `<target>`
argument, enabling usage on single-file applications.
- CLI: `responder run` now also accepts URLs.
### Changed
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed `requests`, `requests-toolbelt`, `rfc3986`, `whitenoise`
- Moved `apispec` and `marshmallow` to `openapi` optional extra
- Replaced `rfc3986` with stdlib `urllib.parse`
- Replaced `requests-toolbelt` multipart decoder with `python-multipart`
- Replaced deprecated `starlette.middleware.wsgi` with `a2wsgi`
- Switched from WhiteNoise to ServeStatic
- Dependencies: Pinned `starlette[full]>=0.40` (was unpinned)
- GraphQL: Upgraded to `graphene>=3` and `graphql-core>=3.1`
(from `graphene<3` and `graphql-server-core`, which is unmaintained)
- GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the `responder.ext` module namespace.
- Packaging: Migrated from `setup.py` to declarative `pyproject.toml`
### Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated `resume_incomplete` and `resume`
aliases for HTTP 308 (marked for removal in 3.0)
- CLI: `responder run --build` ceased to exist
### Fixed
- Routing: Fixed dispatching `static_route=None` on Windows
- uvicorn: `--debug` now maps to uvicorn's `log_level = "debug"`
- Tests: Fixed deprecated httpx TestClient usage
## [v2.0.5] - 2019-12-15
### Added
- Update requirements to support python 3.8
## [v2.0.4] - 2019-11-19
### Fixed
- Fix static app resolving
## [v2.0.3] - 2019-09-20
### Fixed
- Fix template conflicts
## [v2.0.2] - 2019-09-20
### Fixed
- Fix template conflicts
## [v2.0.1] - 2019-09-20
### Fixed
- Fix template import
## [v2.0.0] - 2019-09-19
### Changed
- Refactor Router and Schema
## [v1.3.2] - 2019-08-15
### Added
- ASGI 3 support
- CI tests for python 3.8-dev
- Now requests have `state` a mapping object
### Deprecated
- ASGI 2
## [v1.3.1] - 2019-04-28
### Added
- Route params Converters
- Add search for documentation pages
### Changed
- Bump dependencies
## [v1.3.0] - 2019-02-22
### Fixed
- Versioning issue
- Multiple cookies.
- Whitenoise returns not found.
- Other bugfixes.
### Added
- Stream support via `resp.stream`.
- Cookie directives via `resp.set_cookie`.
- Add `resp.html` to send HTML.
- Other improvements.
## [v1.1.3] - 2019-01-12
### Changed
- Refactor `_route_for`
### Fixed
- Resolve startup/shutdwown events
## [v1.2.0] - 2018-12-29
### Added
- Documentations
### Changed
- Use Starlette's LifeSpan middleware
- Update denpendencies
### Fixed
- Fix route.is_class_based
- Fix test_500
- Typos
## [v1.1.2] - 2018-11-11
### Fixed
- Minor fixes for Open API
- Typos
## [v1.1.1] - 2018-10-29
### Changed
- Run sync views in a threadpoolexecutor.
## [v1.1.0] - 2018-10-27
### Added
- Support for `before_request`.
## [v1.0.5]- 2018-10-27
### Fixed
- Fix sessions.
## [v1.0.4] - 2018-10-27
### Fixed
- Potential bufix for cookies.
## [v1.0.3] - 2018-10-27
### Fixed
- Bugfix for redirects.
## [v1.0.2] - 2018-10-27
### Changed
- Improvement for static file hosting.
## [v1.0.1] - 2018-10-26
### Changed
- Improve cors configuration settings.
## [v1.0.0] - 2018-10-26
### Changed
- Move GraphQL support into a built-in plugin.
## [v0.3.3] - 2018-10-25
### Added
- CORS support
### Changed
- Improved exceptions.
## [v0.3.2] - 2018-10-25
### Changed
- Subtle improvements.
## [v0.3.1] - 2018-10-24
### Fixed
- Packaging fix.
## [v0.3.0] - 2018-10-24
### Changed
- Interactive Documentation endpoint.
- Minor improvements.
## [v0.2.3] - 2018-10-24
### Changed
- Overall improvements.
## [v0.2.2] - 2018-10-23
### Added
- Show traceback info when background tasks raise exceptions.
## [v0.2.1] - 2018-10-23
### Added
- api.requests.
## [v0.2.0] - 2018-10-22
### Added
- WebSocket support.
## [v0.1.6] - 2018-10-20
### Added
- 500 support.
## [v0.1.5] - 2018-10-20
### Added
- File upload support
### Changed
- Improvements to sequential media reading.
## [v0.1.4] - 2018-10-19
### Fixed
- Stability.
## [v0.1.3] - 2018-10-18
### Added
- Sessions support.
## [v0.1.2] - 2018-10-18
### Added
- Cookies support.
## [v0.1.1] - 2018-10-17
### Changed
- Default routes.
## [v0.1.0] - 2018-10-17
### Added
- Prototype of static application support.
## [v0.0.10] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
## [v0.0.9] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
## [v0.0.8] - 2018-10-17
### Added
- GraphiQL Support.
### Changed
- Improvement to route selection.
## [v0.0.7] - 2018-10-16
### Changed
- Immutable Request object.
## [v0.0.6] - 2018-10-16
### Added
- Ability to mount WSGI apps.
- Supply content-type when serving up the schema.
## [v0.0.5] - 2018-10-15
### Added
- OpenAPI Schema support.
- Safe load/dump yaml.
## [v0.0.4] - 2018-10-15
### Added
- Asynchronous support for data uploads.
### Fixed
- Bug fixes.
## [v0.0.3] - 2018-10-13
### Fixed
- Bug fixes.
## [v0.0.2] - 2018-10-13
### Changed
- Switch to ASGI/Starlette.
## [v0.0.1] - 2018-10-12
### Added
- Conception!
[Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.2..HEAD
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
[unreleased]: https://github.com/kennethreitz/responder/compare/v3.0.0..HEAD
[v3.0.0]: https://github.com/kennethreitz/responder/compare/v2.0.5..v3.0.0
[v2.0.5]: https://github.com/kennethreitz/responder/compare/v2.0.4..v2.0.5
[v2.0.4]: https://github.com/kennethreitz/responder/compare/v2.0.3..v2.0.4
[v2.0.3]: https://github.com/kennethreitz/responder/compare/v2.0.2..v2.0.3
[v2.0.2]: https://github.com/kennethreitz/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/kennethreitz/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/kennethreitz/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/kennethreitz/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/kennethreitz/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/kennethreitz/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/kennethreitz/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/kennethreitz/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/kennethreitz/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/kennethreitz/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/kennethreitz/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/kennethreitz/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/kennethreitz/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/kennethreitz/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/kennethreitz/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/kennethreitz/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/kennethreitz/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/kennethreitz/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/kennethreitz/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/kennethreitz/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/kennethreitz/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/kennethreitz/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/kennethreitz/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/kennethreitz/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/kennethreitz/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/kennethreitz/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/kennethreitz/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/kennethreitz/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/kennethreitz/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/kennethreitz/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/kennethreitz/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/kennethreitz/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/kennethreitz/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/kennethreitz/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/kennethreitz/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/kennethreitz/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/kennethreitz/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/kennethreitz/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/kennethreitz/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/kennethreitz/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/kennethreitz/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/kennethreitz/responder/compare/v0.0.0..v0.0.1
+175 -10
View File
@@ -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
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-1
View File
@@ -1 +0,0 @@
include LICENSE
-20
View File
@@ -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
-755
View File
@@ -1,755 +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"
},
"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"
}
}
}
+67 -40
View File
@@ -1,70 +1,97 @@
# Responder: a familiar HTTP Service Framework for Python
[![Build Status](https://travis-ci.org/taoufik07/responder.svg?branch=master)](https://travis-ci.org/taoufik07/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![image](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![image](https://img.shields.io/github/contributors/taoufik07/responder.svg)](https://github.com/taoufik07/responder/graphs/contributors)
[![ci-tests](https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg)](https://github.com/kennethreitz/responder/actions/workflows/test.yaml)
[![ci-docs](https://github.com/kennethreitz/responder/actions/workflows/docs.yaml/badge.svg)](https://github.com/kennethreitz/responder/actions/workflows/docs.yaml)
[![Documentation Status](https://github.com/kennethreitz/responder/actions/workflows/pages/pages-build-deployment/badge.svg)](https://responder.kennethreitz.org/)
[![version](https://img.shields.io/pypi/v/responder.svg)](https://pypi.org/project/responder/)
[![license](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![python-versions](https://img.shields.io/pypi/pyversions/responder.svg)](https://pypi.org/project/responder/)
[![downloads](https://static.pepy.tech/badge/responder/month)](https://pepy.tech/project/responder)
[![contributors](https://img.shields.io/github/contributors/kennethreitz/responder.svg)](https://github.com/kennethreitz/responder/graphs/contributors)
[![status](https://img.shields.io/pypi/status/responder.svg)](https://pypi.org/project/responder/)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
[![responder-synopsis](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
Responder is powered by [Starlette](https://www.starlette.io/).
[View documentation](https://responder.readthedocs.io).
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](https://responder.readthedocs.io).
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.
Responder gets you an ASGI app, with a production static files server pre-installed,
Jinja templating, and a production webserver based on uvloop, automatically serving
up requests with gzip compression.
The `async` declaration within the example program is optional.
## Testimonials
> "Pleasantly very taken with python-responder. [@kennethreitz](https://twitter.com/kennethreitz) at his absolute best." —Rudraksh M.K.
> "Pleasantly very taken with python-responder.
> [@kennethreitz](https://x.com/kennethreitz42) at his absolute best." —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." — Tom Christie author of [Django REST Framework](https://www.django-rest-framework.org/)
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of [Two Scoops of Django]()
> "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/)
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of
> [Two Scoops of Django](https://www.feldroy.com/two-scoops-press#two-scoops-of-django)
## More Examples
See [the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html) for more details on features available in Responder.
See
[the documentation's feature tour](https://responder.readthedocs.io/tour.html)
for more details on features available in Responder.
# Installing Responder
Install the stable release:
Install the most recent stable release:
pip install --upgrade responder
$ pipenv install responder
✨🍰✨
Alternatively, install directly from the repository:
pip install 'responder @ git+https://github.com/kennethreitz/responder.git'
Or, install from the development branch:
$ pipenv install -e git+https://github.com/taoufik07/responder.git#egg=responder
Only **Python 3.6+** is supported.
Responder supports **Python 3.9+**.
# The Basic Idea
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
The primary concept here is to bring the niceties from both Flask and Falcon and
unify them into a single framework. You'll find a familiar API with a clean,
Pythonic design.
- Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- Setting `resp.content` sends back bytes.
- Use `resp.file("path")` to serve files with automatic content-type detection.
- Case-insensitive `req.headers` dict.
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## Features
## Ideas
- Flask-style route expressions with f-string syntax and type convertors
(`str`, `int`, `float`, `uuid`, `path`).
- HTTP method filtering: `@api.route("/data", methods=["GET"])`.
- Every request and response is passed into each view and mutated — including
`response.media` for JSON/YAML content negotiation.
- Built-in test client powered by Starlette's TestClient.
- Mount other WSGI/ASGI apps at subroutes.
- Automatic gzip compression.
- Class-based views with `on_get`, `on_post`, `on_request` methods.
- GraphQL support via Graphene with `api.graphql()`.
- OpenAPI schema generation with interactive docs.
- Lifespan context managers for startup/shutdown.
- Custom exception handlers.
- Before-request hooks with short-circuit support.
- Cookie-based sessions.
- WebSocket support.
- Background tasks.
- Production uvicorn server built-in.
- 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.
## Development
See [Development Sandbox](https://responder.kennethreitz.org/sandbox.html).
## Supported by
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport)
Special thanks to the kind people at JetBrains s.r.o. for supporting us with
excellent development tooling.
-48
View File
@@ -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
+4 -4
View File
@@ -1,7 +1,7 @@
/* Hide module name and default value for environment variable section */
div[id$='environment-variables'] code.descclassname {
display: none;
div[id$="environment-variables"] code.descclassname {
display: none;
}
div[id$='environment-variables'] em.property {
display: none;
div[id$="environment-variables"] em.property {
display: none;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
View File
@@ -10,142 +10,152 @@
*/
var Konami = function (callback) {
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
}
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = '';
e.preventDefault();
return false;
}
},
load: function (link) {
this._currentLink = link;
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, 'keydown', this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
touchendHandler: function () {
konami.iphone.input.push(konami.iphone.check_direction());
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
if (konami.iphone.input.length === konami.iphone.keys.length) {
var match = true;
for (var i = 0; i < konami.iphone.keys.length; i++) {
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
match = false;
}
}
if (match) {
konami.iphone.code(konami._currentLink);
}
}
},
touchstartHandler: function (e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function (link) {
this.orig_keys = this.keys;
konami.addEvent(document, "touchmove", this.touchmoveHandler);
konami.addEvent(document, "touchend", this.touchendHandler, false);
konami.addEvent(document, "touchstart", this.touchstartHandler);
},
unload: function () {
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
konami.removeEvent(document, 'touchend', this.touchendHandler);
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
},
check_direction: function () {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
result = (x_magnitude > y_magnitude) ? x : y;
result = (this.tap === true) ? "TAP" : result;
return result;
}
var konami = {
addEvent: function (obj, type, fn, ref_obj) {
if (obj.addEventListener) obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event, ref_obj);
};
obj.attachEvent("on" + type, obj[type + fn]);
}
},
removeEvent: function (obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: "",
pattern: "38384040373937396665",
keydownHandler: function (e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = "";
e.preventDefault();
return false;
}
},
load: function (link) {
this._currentLink = link;
this.addEvent(document, "keydown", this.keydownHandler, this);
this.iphone.load(link);
},
unload: function () {
this.removeEvent(document, "keydown", this.keydownHandler);
this.iphone.unload();
},
code: function (link) {
window.location = link;
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: "",
keys: [
"UP",
"UP",
"DOWN",
"DOWN",
"LEFT",
"RIGHT",
"LEFT",
"RIGHT",
"TAP",
"TAP",
],
input: [],
code: function (link) {
konami.code(link);
},
touchmoveHandler: function (e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
}
},
touchendHandler: function () {
konami.iphone.input.push(konami.iphone.check_direction());
typeof callback === "string" && konami.load(callback);
if (typeof callback === "function") {
konami.code = callback;
konami.load();
}
if (konami.iphone.input.length > konami.iphone.keys.length)
konami.iphone.input.shift();
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') {
module.exports = Konami;
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = Konami;
} else {
if (typeof define === 'function' && define.amd) {
define([], function() {
return Konami;
});
} else {
window.Konami = Konami;
}
if (typeof define === "function" && define.amd) {
define([], function () {
return Konami;
});
} else {
window.Konami = Konami;
}
}
+73 -37
View File
@@ -1,5 +1,13 @@
<link rel="stylesheet" type="text/css" href="https://cloud.typography.com/7584432/7586812/css/fonts.css" />
<script type="text/javascript">$('#searchbox').hide(0);</script>
<link
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 -->
<!-- CSS Adjustments (I'm very picky.) -->
@@ -39,9 +47,7 @@
}
.method {
margin-bottom: 2em;
}
.si,
@@ -80,8 +86,6 @@
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
display: none;
@@ -118,10 +122,12 @@
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127383416-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag('config', 'UA-127383416-1');
gtag("config", "UA-127383416-1");
</script>
<!-- There are no more hacks. -->
@@ -130,7 +136,10 @@
<script src="{{ pathto('_static/', 1) }}/konami.js"></script>
<script>
var easter_egg = new Konami('https://www.myfortunecookie.co.uk/fortunes/' + (Math.floor(Math.random() * 152) + 1));
var easter_egg = new Konami(
"https://www.myfortunecookie.co.uk/fortunes/" +
(Math.floor(Math.random() * 152) + 1)
);
</script>
<style>
@@ -140,67 +149,94 @@
</style>
<!-- GitHub Logo -->
<a href="https://github.com/kennethreitz/responder" class="github-corner" aria-label="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
aria-hidden="true">
<a
href="https://github.com/kennethreitz/responder"
class="github-corner"
aria-label="View source on GitHub"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill: #151513; color: #fff; position: absolute; top: 0; border: 0; right: 0"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor" class="octo-body"></path>
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
></path>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
></path>
</svg>
</a>
<style>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0)
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg)
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg)
transform: rotate(10deg);
}
}
@media (max-width:500px) {
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out
animation: octocat-wave 560ms ease-in-out;
}
}
</style>
<!-- That was not a hack. That was art.
<!-- UserVoice JavaScript SDK (only needed once on a page) -->
<script>(function () { var uv = document.createElement('script'); uv.type = 'text/javascript'; uv.async = true; uv.src = '//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(uv, s) })()</script>
<script>
(function () {
var uv = document.createElement("script");
uv.type = "text/javascript";
uv.async = true;
uv.src = "//widget.uservoice.com/f4AQraEfwInlMzkexfRLg.js";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(uv, s);
})();
</script>
<!-- A tab to launch the Classic Widget -->
<script>
UserVoice = window.UserVoice || [];
UserVoice.push(['showTab', 'classic_widget', {
mode: 'feedback',
primary_color: '#fa8c28',
link_color: '#0a8cc6',
forum_id: 913660,
tab_label: 'Got feedback?',
tab_color: '#00994f',
tab_position: 'bottom-left',
tab_inverted: true
}]);
UserVoice.push([
"showTab",
"classic_widget",
{
mode: "feedback",
primary_color: "#fa8c28",
link_color: "#0a8cc6",
forum_id: 913660,
tab_label: "Got feedback?",
tab_color: "#00994f",
tab_position: "bottom-left",
tab_inverted: true,
},
]);
</script>
+63 -29
View File
@@ -1,54 +1,88 @@
<p class="logo">
<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>
</p>
<p>
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
<a class="github-button"
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>
<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>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
})"
async
></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
@kennethreitz</a>
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
<p>
<a class="github-button"
href="https://github.com/kennethreitz"
data-color-scheme="no-preference: light; light: light; dark: light;"
data-size="medium"
data-show-count="true"
aria-label="Follow @kennethreitz on GitHub">Follow @kennethreitz</a>
</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>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
+67 -28
View File
@@ -1,54 +1,93 @@
<p class="logo">
<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>
</p>
<p>
<iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&repo=responder&type=watch&count=true&size=large"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200px"
height="35px"
></iframe>
</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css"
/>
<style>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
})"
async
></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<p><strong>Responder</strong> is a web service framework, written for human beings.</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true" allowtransparency="true"
frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow
@kennethreitz</a>
<script>!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs');</script>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=true"
allowtransparency="true"
frameborder="0"
scrolling="0"
width="200"
height="20"
></iframe>
</p>
<p>
<a
href="https://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>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
+7
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
../../CHANGELOG.md
+174
View File
@@ -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
View File
@@ -20,7 +20,7 @@
# -- Project information -----------------------------------------------------
project = "responder"
copyright = "2018, A Kenneth Reitz project"
copyright = "2018-2026, A Kenneth Reitz project"
author = "Kenneth Reitz"
# The short X.Y version
@@ -57,6 +57,11 @@ extensions = [
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode",
"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.
@@ -66,7 +71,7 @@ templates_path = ["_templates"]
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
source_suffix = {".rst": "restructuredtext"}
# The master toctree document.
master_doc = "index"
@@ -76,7 +81,7 @@ master_doc = "index"
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -211,12 +216,77 @@ epub_exclude_files = ["search.html"]
# -- 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 ---------------------------------------
# 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 ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
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
View File
@@ -6,33 +6,30 @@ You can deploy Responder anywhere you can deploy a basic Python application.
Docker Deployment
-----------------
Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
Assuming an existing ``api.py`` containing your Responder application.
``Dockerfile``::
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install responder
ENV PORT=80
EXPOSE 80
CMD ["python", "api.py"]
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::
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder::
$ pipenv install responder
...
Write out an ``api.py``::
@@ -47,12 +44,12 @@ Write out an ``api.py``::
if __name__ == "__main__":
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
$ git commit -m 'initial commit'
$ git push heroku master
uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
+66 -33
View File
@@ -6,19 +6,23 @@
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
:target: https://travis-ci.org/kennethreitz/responder
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
.. |ci-tests| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
.. |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/
.. |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/
.. |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/
.. |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
.. |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
.. code:: python
@@ -34,23 +38,24 @@ A familiar HTTP Service Framework
if __name__ == '__main__':
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
(`WhiteNoise <http://whitenoise.evans.io/en/stable/>`_)
pre-installed, jinja2 templating (without additional imports), and a
production webserver based on uvloop, serving up requests with
automatic gzip compression.
The example program demonstrates an `ASGI`_ application using `Responder`_,
including production-ready components like the `uvicorn`_ webserver, based
on `uvloop`_, and the `Jinja`_ templating library pre-installed.
The ``async`` declaration within the example program is optional.
Features
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritance.
- `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
- `ASGI`_, the future of Python web services.
- Asynchronous Python frameworks and applications.
- Automatic gzip compression.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- `f-string syntax <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
- `f-string syntax`_ route declaration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
@@ -61,30 +66,23 @@ Testimonials
------------
“Pleasantly very taken with python-responder.
`@kennethreitz <https://twitter.com/kennethreitz>`_ at his absolute
best.”
—Rudraksh M.K.
`@kennethreitz`_ at his absolute best.”
— 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."
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!”
— 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
-----------
@@ -96,17 +94,38 @@ User Guides
deployment
testing
api
cli
Installing Responder
--------------------
Use ``uv`` for fast installation.
.. 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
@@ -123,14 +142,14 @@ The primary concept here is to bring the niceties that are brought forth from bo
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.
- **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.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- `Uvicorn <https://www.uvicorn.org/>`_ is built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against `slowloris <https://en.wikipedia.org/wiki/Slowloris_(computer_security)>`_ attacks, making nginx unnecessary in production.
- `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.
@@ -140,3 +159,17 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :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/
+11 -11
View File
@@ -54,7 +54,7 @@ Type convertors are also available::
async def add(req, resp, *, 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
---------------------
@@ -73,7 +73,7 @@ If the client requests YAML instead (with a header of ``Accept: application/x-ya
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::
@@ -85,9 +85,10 @@ Usage::
def hello(req, resp, name):
resp.html = templates.render("hello.html", name=name)
Also a ``render_async`` is available::
templates = Templates(enable_async=True)
resp.html = await templates.render_async("hello.html", who=who)
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
@@ -157,20 +158,19 @@ Here's a sample code to post a file with background::
@api.background.task
def process_data(data):
f = open('./{}'.format(data['file']['filename']), 'w')
f.write(data['file']['content'].decode('utf-8'))
f.close()
with open(f"./{data['file']['filename']}", 'wb') as f:
f.write(data['file']['content'])
data = await req.media(format='files')
process_data(data)
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/
+35
View File
@@ -0,0 +1,35 @@
(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 runtime extensions and development tools.
```shell
uv pip install --upgrade --editable '.[develop,docs,release,test]'
```
## Operations
Invoke linter and software tests.
```shell
source .venv/bin/activate
poe check
```
Format code.
```shell
poe format
```
Documentation authoring.
```shell
poe docs-autobuild
```
+4 -37
View File
@@ -1,16 +1,16 @@
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
----------
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``::
@@ -25,26 +25,6 @@ Your repository should look like this::
if __name__ == "__main__":
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
-------------
@@ -66,16 +46,3 @@ Writing Tests
...
========================== 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
View File
@@ -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
-----------------
@@ -15,6 +30,61 @@ Class-based views (and setting some headers and stuff)::
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
----------------
@@ -34,10 +104,15 @@ Here, you can spawn off a background thread to run any function, out-of-request:
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::
import graphene
from responder.ext.graphql import GraphQLView
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
@@ -46,7 +121,7 @@ Serve a GraphQL API::
return f"Hello {name}"
schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
view = GraphQLView(api=api, schema=schema)
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
----------------------
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
from responder.ext.schema import Schema as OpenAPISchema
from responder.ext.openapi import OpenAPISchema
from marshmallow import Schema, fields
contact = {
@@ -194,12 +275,11 @@ Responder can automatically supply API Documentation for you. Using the example
The new and recommended way::
...
from responder.ext.schema import Schema
...
from responder.ext.openapi import OpenAPISchema
api = responder.API()
schema = Schema(
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
@@ -214,7 +294,7 @@ The new and recommended way::
)
The old way ::
The old way::
api = responder.API(
title="Web Service",
@@ -283,7 +363,7 @@ Supported directives:
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives>`_
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_
Using Cookie-Based Sessions
@@ -353,7 +433,7 @@ Closing the connection::
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)::
+19
View File
@@ -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()
+26
View File
@@ -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()
+25
View File
@@ -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()
+230
View File
@@ -0,0 +1,230 @@
[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 = [
"poethepoet",
"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
[tool.poe.tasks]
check = [
"test",
]
docs-autobuild = [
{ cmd = "sphinx-autobuild --open-browser --watch docs/source docs/source docs/build" },
]
docs-html = [
{ cmd = "sphinx-build -W --keep-going docs/source docs/build" },
]
docs-linkcheck = [
{ cmd = "sphinx-build -W --keep-going -b linkcheck docs/source docs/build" },
]
format = [
{ cmd = "ruff format ." },
# Configure Ruff not to auto-fix (remove!):
# unused imports (F401), unused variables (F841), `print` statements (T201), and commented-out code (ERA001).
{ cmd = "ruff check --fix --ignore=ERA --ignore=F401 --ignore=F841 --ignore=T20 --ignore=ERA001 ." },
{ cmd = "pyproject-fmt --keep-full-version pyproject.toml" },
]
lint = [
{ cmd = "ruff format --check ." },
{ cmd = "ruff check ." },
{ cmd = "validate-pyproject pyproject.toml" },
{ cmd = "mypy" },
]
release = [
{ cmd = "python -m build" },
{ cmd = "twine upload --skip-existing dist/*" },
]
[tool.poe.tasks.test]
cmd = "pytest"
help = "Invoke software tests"
[tool.poe.tasks.test.args.expression]
options = [ "-k" ]
[tool.poe.tasks.test.args.marker]
options = [ "-m" ]
-4
View File
@@ -1,4 +0,0 @@
[pytest]
;addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
-5
View File
@@ -1,5 +0,0 @@
build:
image: latest
python:
version: 3.6
+17 -1
View File
@@ -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 .__version__ import __version__
from .core import API, Request, Response
__all__ = [
"API",
"Request",
"Response",
"__version__",
"ext",
]
-4
View File
@@ -1,4 +0,0 @@
from .cli import main
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.0.2"
__version__ = "3.0.0"
+162 -75
View File
@@ -1,41 +1,37 @@
import json
import asyncio
import os
from pathlib import Path
import jinja2
__all__ = ["API"]
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.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Lifespan
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
from starlette.middleware.trustedhost import TrustedHostMiddleware
from . import models, status_codes
from . import status_codes
from .background import BackgroundQueue
from .formats import get_formats
from .models import Request, Response
from .routes import Router
from .statics import DEFAULT_API_THEME, DEFAULT_CORS_PARAMS, DEFAULT_SECRET_KEY
from .ext.schema import Schema as OpenAPISchema
from .staticfiles import StaticFiles
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
from .templates import Templates
class API:
"""The primary web-service class.
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
"""
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param auto_escape: If ``True``, HTML and XML templates will automatically be escaped.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
:param openapi_theme: OpenAPI documentation theme, must be one of ``elements``, ``rapidoc``, ``redoc``, ``swagger_ui``
""" # noqa: E501
status_codes = status_codes
@@ -48,7 +44,7 @@ class API:
description=None,
terms_of_service=None,
contact=None,
license=None,
license=None, # noqa: A002
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
@@ -61,17 +57,19 @@ class API:
cors=False,
cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None,
openapi_theme=DEFAULT_OPENAPI_THEME,
lifespan=None,
):
self.background = BackgroundQueue()
self.secret_key = secret_key
self.router = Router()
self.router = Router(lifespan=lifespan)
if static_dir is not None:
if static_route is None:
static_route = static_dir
static_dir = Path(os.path.abspath(static_dir))
static_route = ""
static_dir = Path(static_dir).resolve()
self.static_dir = static_dir
self.static_route = static_route
@@ -82,22 +80,15 @@ class API:
self.debug = debug
if not allowed_hosts:
# if not debug:
# raise RuntimeError(
# "You need to specify `allowed_hosts` when debug is set to False"
# )
allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts
if self.static_dir is not None:
os.makedirs(self.static_dir, exist_ok=True)
if self.static_dir is not None:
self.static_dir.mkdir(parents=True, exist_ok=True)
self.mount(self.static_route, self.static_app)
self.formats = get_formats()
# Cached requests session.
self._session = None
self.default_endpoint = None
@@ -115,11 +106,19 @@ class API:
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
if openapi or docs_route:
try:
from .ext.openapi import OpenAPISchema
except ImportError as ex:
raise ImportError(
"The dependencies for the OpenAPI extension are not installed. "
"Install them using: pip install responder"
) from ex
self.openapi = OpenAPISchema(
app=self,
title="Web Service",
version="1.0",
openapi="3.0.2",
title=title,
version=version,
openapi=openapi,
docs_route=docs_route,
description=description,
terms_of_service=terms_of_service,
@@ -127,13 +126,15 @@ class API:
license=license,
openapi_route=openapi_route,
static_route=static_route,
openapi_theme=openapi_theme,
)
# TODO: Update docs for templates
self.templates = Templates(directory=templates_dir)
self.requests = (
self.session()
) #: A Requests session that is connected to the ASGI app.
@property
def requests(self):
"""A test client connected to the ASGI app. Lazily initialized."""
return self.session()
@property
def static_app(self):
@@ -152,9 +153,59 @@ class API:
def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config)
def schema(self, name, **options):
"""Decorator for creating new routes around function and class definitions.
def exception_handler(self, exception_cls):
"""Register a handler for a specific exception type.
Usage::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
"""
def decorator(func):
async def _handler(request, exc):
from starlette.responses import Response as StarletteResp
req = Request(request.scope, request.receive, formats=get_formats())
resp = Response(req=req, formats=get_formats())
if asyncio.iscoroutinefunction(func):
await func(req, resp, exc)
else:
func(req, resp, exc)
if resp.status_code is None:
resp.status_code = 500
body, headers = await resp.body
return StarletteResp(
content=body, status_code=resp.status_code, headers=headers
)
# Register with the ExceptionMiddleware
self.router._exception_handlers = getattr(
self.router, "_exception_handlers", {}
)
self.router._exception_handlers[exception_cls] = _handler
# Also register on the ASGI app chain
from starlette.middleware.exceptions import ExceptionMiddleware as EM
app = self.app
while app is not None:
if isinstance(app, EM):
app.add_exception_handler(exception_cls, _handler)
break
app = getattr(app, "app", None)
return func
return decorator
def schema(self, name, **options):
"""
Decorator for creating new routes around function and class definitions.
Usage::
from marshmallow import Schema, fields
@api.schema("Pet")
class PetSchema(Schema):
@@ -171,11 +222,12 @@ class API:
"""Given a path portion of a URL, tests that it matches against any registered route.
:param path: The path portion of a URL, to test all known routes against.
"""
""" # noqa: E501 (Line too long)
for route in self.router.routes:
match, _ = route.matches(path)
if match:
return route
return None
def add_route(
self,
@@ -187,16 +239,18 @@ class API:
check_existing=True,
websocket=False,
before_request=False,
methods=None,
):
"""Adds a route to the API.
:param route: A string representation of the route.
:param endpoint: The endpoint for the route -- can be a callable, or a class.
:param default: If ``True``, all unknown requests will route to this view.
:param static: If ``True``, and no endpoint was passed, render "static/index.html", 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:
assert self.static_dir is not None
if not endpoint:
@@ -210,27 +264,35 @@ class API:
websocket=websocket,
before_request=before_request,
check_existing=check_existing,
methods=methods,
)
async def _static_response(self, req, resp):
assert self.static_dir is not None
index = (self.static_dir / "index.html").resolve()
if os.path.exists(index):
with open(index, "r") as f:
resp.html = "Hello world !"
if index.exists():
resp.html = index.read_text()
else:
resp.status_code = status_codes.HTTP_404
resp.status_code = status_codes.HTTP_404 # type: ignore[attr-defined]
resp.text = "Not found."
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 location: The location of the redirect.
:param set_text: If ``True``, sets the Redirect body content automatically.
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
:param status_code: an `API.status_codes` attribute, or an integer,
representing the HTTP status code of the redirect.
"""
resp.redirect(location, set_text=set_text, status_code=status_code)
@@ -263,7 +325,7 @@ class API:
: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):
"""Decorator for creating new routes around function and class definitions.
@@ -282,26 +344,50 @@ class API:
return decorator
def graphql(self, route="/graphql", *, schema):
"""Mount a GraphQL API at the given route.
Usage::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
:param route: The URL path for the GraphQL endpoint.
:param schema: A Graphene schema instance.
"""
from .ext.graphql import GraphQLView
self.add_route(route, GraphQLView(api=self, schema=schema))
def mount(self, route, app):
"""Mounts an WSGI / ASGI application at a given route.
:param route: String representation of the route to be used (shouldn't be parameterized).
:param route: String representation of the route to be used
(shouldn't be parameterized).
:param app: The other WSGI / ASGI app.
"""
self.router.apps.update({route: app})
def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a 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:
from starlette.testclient import TestClient
self._session = TestClient(self, base_url=base_url)
return self._session
def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
:param endpoint: The route endpoint you're searching for.
@@ -310,48 +396,49 @@ class API:
return self.router.url_for(endpoint, **params)
def template(self, filename, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
r"""Render a Jinja2 template file with the provided values.
:param filename: The filename of the jinja2 template, in ``templates_dir``.
:param *args: Data to pass into the template.
:param *kwargs: Date to pass into the template.
:param \*args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template.
"""
return self.templates.render(filename, *args, **kwargs)
def template_string(self, source, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
Note: The current ``api`` instance is by default passed into the view. This is set in the dict ``api.jinja_values_base``.
:param source: The template to use.
:param *args: Data to pass into the template.
:param **kwargs: Data to pass into the template.
r"""Render a Jinja2 template string with the provided values.
:param source: The template to use, a Jinja2 template string.
:param \*args: Data to pass into the template.
:param \*\*kwargs: Data to pass into the template.
"""
return self.templates.render_string(source, *args, **kwargs)
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
known hosts.
"""
Run the application with uvicorn.
If the ``PORT`` environment variable is set, requests will be served on that port
automatically to all known hosts.
:param address: The address to bind to.
:param port: The port to bind to. If none is provided, one will be selected at random.
:param debug: 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()``.
"""
""" # noqa: E501
if "PORT" in os.environ:
if address is None:
address = "0.0.0.0"
address = "0.0.0.0" # noqa: S104
port = int(os.environ["PORT"])
if address is None:
address = "127.0.0.1"
if port is None:
port = 5042
if debug:
options["log_level"] = "debug"
def spawn():
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
uvicorn.run(self, host=address, port=port, **options)
def run(self, **kwargs):
if "debug" not in kwargs:
+6 -8
View File
@@ -1,10 +1,12 @@
import asyncio
import functools
import concurrent.futures
import multiprocessing
import traceback
from starlette.concurrency import run_in_threadpool
__all__ = ["BackgroundQueue"]
class BackgroundQueue:
def __init__(self, n=None):
@@ -16,9 +18,6 @@ class BackgroundQueue:
self.results = []
def run(self, f, *args, **kwargs):
self.pool._max_workers = self.n
self.pool._adjust_thread_count()
f = self.pool.submit(f, *args, **kwargs)
self.results.append(f)
return f
@@ -27,7 +26,7 @@ class BackgroundQueue:
def on_future_done(fs):
try:
fs.result()
except:
except Exception:
traceback.print_exc()
def do_task(*args, **kwargs):
@@ -39,6 +38,5 @@ class BackgroundQueue:
async def __call__(self, func, *args, **kwargs) -> None:
if asyncio.iscoroutinefunction(func):
return await asyncio.ensure_future(func(*args, **kwargs))
else:
return await run_in_threadpool(func, *args, **kwargs)
return await asyncio.create_task(func(*args, **kwargs))
return await run_in_threadpool(func, *args, **kwargs)
-43
View File
@@ -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
View File
@@ -1,3 +1,8 @@
from .api import API
from .models import Request, Response
from .cli import cli
__all__ = [
"API",
"Request",
"Response",
]
-1
View File
@@ -1 +0,0 @@
from .graphql import GraphQLView
+129
View File
@@ -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"
)
+24 -28
View File
@@ -1,7 +1,4 @@
import json
from functools import partial
from graphql_server import default_format_error, encode_execution_results, json_encode
from .templates import GRAPHIQL
@@ -12,24 +9,19 @@ class GraphQLView:
self.schema = schema
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
async def _resolve_graphql_query(req, resp):
if "json" in req.mimetype:
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 (
json_media["query"],
json_media.get("variables"),
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.
if "query" in req.params:
return req.params["query"], None, None
@@ -37,34 +29,38 @@ class GraphQLView:
return req.params["q"], None, None
# Otherwise, the request text is used (typical).
# TODO: Make some assertions about content-type here.
return req.text, None, None
return await 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")
if show_graphiql:
resp.content = self.api.templates.render_string(
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}
result = schema.execute(
result = self.schema.execute(
query, variables=variables, operation_name=operation_name, context=context
)
result, status_code = encode_execution_results(
[result],
is_batch=False,
format_error=default_format_error,
encode=partial(json_encode, pretty=False),
)
resp.media = json.loads(result)
return (query, result, status_code)
response_data = {}
if result.errors:
response_data["errors"] = [{"message": str(e)} for e in result.errors]
if result.data is not None:
response_data["data"] = result.data
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):
await self.graphql_response(req, resp, self.schema)
await self.graphql_response(req, resp)
async def __call__(self, req, resp):
await self.on_request(req, resp)
+10 -121
View File
@@ -1,13 +1,8 @@
# ruff: noqa: E501
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>
<html>
<head>
@@ -22,123 +17,17 @@ GRAPHIQL = """
height: 100vh;
}
</style>
<!--
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>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ GRAPHIQL_VERSION }}/graphiql.min.css" rel="stylesheet"/>
</head>
<body>
<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>
/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams) {
// This example expects a GraphQL server at the path /graphql.
// Change this to point wherever you host your GraphQL server.
return fetch('{{ endpoint }}', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
const fetcher = GraphiQL.createFetcher({ url: '{{ endpoint }}' });
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
root.render(React.createElement(GraphiQL, { fetcher: fetcher }));
</script>
</body>
</html>
@@ -1,18 +1,14 @@
import os
from pathlib import Path
import apistar
import jinja2
import yaml
from apispec import APISpec, yaml_utils
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.statics import API_THEMES, DEFAULT_OPENAPI_THEME
from responder.templates import Templates
class Schema:
class OpenAPISchema:
def __init__(
self,
app,
@@ -22,11 +18,12 @@ class Schema:
description=None,
terms_of_service=None,
contact=None,
license=None,
license=None, # noqa: A002
openapi=None,
openapi_route="/schema.yml",
docs_route="/docs/",
static_route="/static",
openapi_theme=DEFAULT_OPENAPI_THEME,
):
self.app = app
self.schemas = {}
@@ -40,7 +37,9 @@ class Schema:
self.openapi_version = openapi
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.plugins = [MarshmallowPlugin()] if plugins is None else plugins
@@ -51,17 +50,13 @@ class Schema:
if self.docs_route is not None:
self.app.add_route(self.docs_route, self.docs_response)
theme_path = (
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
).resolve()
theme_path = (Path(__file__).parent / "docs").resolve()
self.templates = Templates(directory=theme_path)
self.static_route = static_route
self.app.static_app.add_directory(theme_path)
@property
def _apispec(self):
info = {}
if self.description is not None:
info["description"] = self.description
@@ -82,9 +77,7 @@ class Schema:
for route in self.app.router.routes:
if route.description:
operations = yaml_utils.load_operations_from_docstring(
route.description
)
operations = yaml_utils.load_operations_from_docstring(route.description)
spec.path(path=route.route, operations=operations)
for name, schema in self.schemas.items():
@@ -97,7 +90,7 @@ class Schema:
return self._apispec.to_yaml()
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:
assert name not in self.schemas
@@ -124,25 +117,10 @@ class Schema:
@property
def docs(self):
loader = jinja2.PrefixLoader(
{
self.docs_theme: jinja2.PackageLoader(
"apistar", os.path.join("themes", self.docs_theme, "templates")
)
}
)
env = jinja2.Environment(autoescape=True, loader=loader)
document = apistar.document.Document()
document.content = yaml.safe_load(self.openapi)
template = env.get_template("/".join([self.docs_theme, "index.html"]))
return template.render(
document=document,
langs=["javascript", "python"],
code_style=None,
static_url=self.static_url,
return self.templates.render(
f"{self.docs_theme}.html",
title=self.title,
version=self.version,
schema_url="/schema.yml",
)
@@ -155,6 +133,6 @@ class Schema:
resp.html = self.docs
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.content = self.openapi
+24
View File
@@ -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>
+16
View File
@@ -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>
+23
View File
@@ -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
View File
@@ -1,84 +1,142 @@
from __future__ import annotations
import json
from urllib.parse import urlencode
import yaml
from requests_toolbelt.multipart import decoder
from python_multipart import MultipartParser
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):
if encode:
pass
elif "multipart/form-data" in r.headers.get("Content-Type"):
decode = decoder.MultipartDecoder(await r.content, r.mimetype)
querys = list()
for part in decode.parts:
header = part.headers.get(b"Content-Disposition").decode("utf-8")
text = part.text
return None
if "multipart/form-data" in r.headers.get("Content-Type"):
parts = _parse_multipart(await r.content, r.mimetype)
queries = []
for part in parts:
header = part.headers.get("Content-Disposition", "")
text = part.body.decode("utf-8")
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[1]
key = key[1:-1]
querys.append((key, text))
queries.append((key, text))
content = urlencode(querys)
content = urlencode(queries)
return QueryDict(content)
else:
return QueryDict(await r.text)
return QueryDict(await r.text)
async def format_yaml(r, encode=False):
if encode:
r.headers.update({"Content-Type": "application/x-yaml"})
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):
if encode:
r.headers.update({"Content-Type": "application/json"})
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):
if encode:
pass
else:
decoded = decoder.MultipartDecoder(await r.content, r.mimetype)
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
mimetype = part.headers.get(b"Content-Type", None)
filename = None
return None
parts = _parse_multipart(await r.content, r.mimetype)
dump = {}
for part in parts:
header = part.headers.get("Content-Disposition", "")
mimetype = part.headers.get("Content-Type", None)
filename = None
formname = None
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
value = value[1:-1]
value = value[1:-1]
if key == "filename":
filename = value
elif key == "name":
formname = value
if key == "filename":
filename = value
elif key == "name":
formname = value
if formname is None:
continue
if mimetype is None:
dump[formname] = part.content
else:
dump[formname] = {
"filename": filename,
"content": part.content,
"content-type": mimetype.decode("utf-8"),
}
return dump
if mimetype is None:
dump[formname] = part.body
else:
dump[formname] = {
"filename": filename,
"content": part.body,
"content-type": mimetype,
}
return dump
def get_formats():
+127 -43
View File
@@ -1,29 +1,51 @@
from __future__ import annotations
import functools
import io
import inspect
import json
import gzip
from urllib.parse import parse_qs
from base64 import b64decode
from collections.abc import Callable
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, urlparse
import chardet
import rfc3986
import graphene
import yaml
__all__ = ["Request", "Response", "QueryDict"]
from requests.structures import CaseInsensitiveDict
from requests.cookies import RequestsCookieJar
from starlette.datastructures import MutableHeaders
from starlette.requests import Request as StarletteRequest, State
try:
import chardet
except ImportError:
chardet = None # type: ignore[assignment]
from starlette.requests import Request as StarletteRequest
from starlette.requests import State
from starlette.responses import (
Response as StarletteResponse,
)
from starlette.responses import (
StreamingResponse as StarletteStreamingResponse,
)
from .status_codes import HTTP_200, HTTP_301
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):
@@ -112,7 +134,7 @@ class Request:
self.api = api
self._content = None
headers = CaseInsensitiveDict()
headers: CaseInsensitiveDict = CaseInsensitiveDict()
for key, value in self._starlette.headers.items():
headers[key] = value
@@ -133,6 +155,11 @@ class Request:
def mimetype(self):
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
def method(self):
"""The incoming HTTP method used for the request, lower-cased."""
@@ -146,20 +173,20 @@ class Request:
@property
def url(self):
"""The parsed URL of the Request."""
return rfc3986.urlparse(self.full_url)
return urlparse(self.full_url)
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
if self._cookies is None:
cookies = RequestsCookieJar()
cookies = {}
cookie_header = self.headers.get("Cookie", "")
bc = SimpleCookie(cookie_header)
bc: SimpleCookie = SimpleCookie(cookie_header)
for key, morsel in bc.items():
cookies[key] = morsel.value
self._cookies = cookies.get_dict()
self._cookies = cookies
return self._cookies
@@ -171,6 +198,16 @@ class Request:
except AttributeError:
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
def state(self) -> State:
"""
@@ -213,16 +250,23 @@ class Request:
async def declared_encoding(self):
if "Encoding" in self.headers:
return self.headers["Encoding"]
return None
@property
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
if 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
def is_secure(self):
@@ -232,20 +276,31 @@ class Request:
"""Returns ``True`` if the incoming Request accepts the given ``content_type``."""
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.
: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:
format = "yaml" if "yaml" in self.mimetype or "" else "json"
format = "form" if "form" in self.mimetype or "" else format
format = "yaml" if "yaml" in self.mimetype or "" else "json" # noqa: A001
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:
return await format(self)
raise TypeError(f"Invalid 'format' argument: {format}")
return await formatter(self)
def content_setter(mimetype):
@@ -279,24 +334,22 @@ class Response:
def __init__(self, req, *, formats):
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.mimetype = None
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.headers = (
{}
) #: A Python dictionary of ``{key: value}``, representing the headers of the response.
self.headers = {} #: A Python dictionary of ``{key: value}``,
#: representing the headers of the response.
self.formats = formats
self.cookies = SimpleCookie() #: The cookies set in the Response
self.cookies: SimpleCookie = SimpleCookie() #: The cookies set in the Response
self.session = (
req.session
) #: The cookie-based session data, in dict form, to add to the Response.
# Property or func/dec
def stream(self, func, *args, **kwargs):
assert inspect.isasyncgenfunction(func)
@@ -304,6 +357,25 @@ class Response:
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):
self.status_code = status_code
if set_text:
@@ -322,12 +394,13 @@ class Response:
headers["Content-Type"] = self.mimetype
if self.mimetype == "text/plain" and self.encoding is not None:
headers["Encoding"] = self.encoding
content = content.encode(self.encoding)
if isinstance(content, str):
content = content.encode(self.encoding)
return (content, headers)
for format in self.formats:
if self.req.accepts(format):
return (await self.formats[format](self, encode=True)), {}
for format_ in self.formats:
if self.req.accepts(format_):
return (await self.formats[format_](self, encode=True)), {}
# Default to JSON anyway.
return (
@@ -371,12 +444,23 @@ class Response:
if self.headers:
headers.update(self.headers)
response_cls: type[StarletteResponse] | type[StarletteStreamingResponse]
if self._stream is not None:
response_cls = StarletteStreamingResponse
else:
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)
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
+113 -45
View File
@@ -1,24 +1,28 @@
import asyncio
import json
import re
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.exceptions import HTTPException
from starlette.types import ASGIApp
from starlette.websockets import WebSocket, WebSocketClose
from .models import Request, Response
from . import status_codes
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 = {
"int": (int, r"\d+"),
"str": (str, r"[^/]+"),
"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_]*)?}")
@@ -32,9 +36,9 @@ def compile_path(path):
for match in PARAM_RE.finditer(path):
param_name, convertor_type = match.groups(default="str")
convertor_type = convertor_type.lstrip(":")
assert (
convertor_type in _CONVERTORS.keys()
), f"Unknown path convertor '{convertor_type}'"
assert convertor_type in _CONVERTORS.keys(), (
f"Unknown path convertor '{convertor_type}'"
)
convertor, convertor_re = _CONVERTORS[convertor_type]
path_re += path[idx : match.start()]
@@ -58,19 +62,22 @@ class 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 '/'"
self.route = route
self.endpoint = endpoint
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)
# Strip type annotations for URL generation (e.g. {id:int} -> {id})
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params):
return self.route.format(**params)
return self._url_template.format(**params)
@property
def endpoint_name(self):
@@ -84,6 +91,9 @@ class Route(BaseRoute):
if scope["type"] != "http":
return False, {}
if self.methods and scope.get("method", "").upper() not in self.methods:
return False, {}
path = scope["path"]
match = self.path_re.match(path)
@@ -108,6 +118,10 @@ class Route(BaseRoute):
await before_request(request, response)
else:
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 = []
@@ -121,14 +135,14 @@ class Route(BaseRoute):
try:
view = getattr(endpoint, method_name)
views.append(view)
except AttributeError:
except AttributeError as ex:
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:
views.append(self.endpoint)
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(
view.__call__
):
@@ -137,12 +151,11 @@ class Route(BaseRoute):
await run_in_threadpool(view, request, response, **path_params)
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)
def __eq__(self, other):
# [TODO] compare to str ?
return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self):
@@ -157,12 +170,13 @@ class WebSocketRoute(BaseRoute):
self.before_request = before_request
self.path_re, self.param_convertors = compile_path(route)
self._url_template = PARAM_RE.sub(r"{\1}", route)
def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params):
return self.route.format(**params)
return self._url_template.format(**params)
@property
def endpoint_name(self):
@@ -198,7 +212,6 @@ class WebSocketRoute(BaseRoute):
await self.endpoint(ws)
def __eq__(self, other):
# [TODO] compare to str ?
return self.route == other.route and self.endpoint == other.endpoint
def __hash__(self):
@@ -206,17 +219,20 @@ class WebSocketRoute(BaseRoute):
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)
# [TODO] Make its own router
self.apps = {}
self.apps: dict[str, ASGIApp] = {}
self.default_endpoint = (
self.default_response if default_response is None else default_response
)
self.lifespan_handler = Lifespan()
self.before_requests = (
{"http": [], "ws": []} if before_requests is None else before_requests
)
self.events = defaultdict(list)
self._lifespan_handler = lifespan
def add_route(
self,
@@ -227,11 +243,13 @@ class Router:
websocket=False,
before_request=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 endpoint: The endpoint for the route -- can be callable, or class.
: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 websocket:
@@ -241,9 +259,9 @@ class Router:
return
if check_existing:
assert not self.routes or route not in (
item.route for item in self.routes
), f"Route '{route}' already exists"
assert not self.routes or route not in (item.route for item in self.routes), (
f"Route '{route}' already exists"
)
if default:
self.default_endpoint = endpoint
@@ -251,14 +269,27 @@ class Router:
if websocket:
route = WebSocketRoute(route, endpoint)
else:
route = Route(route, endpoint)
route = Route(route, endpoint, methods=methods)
self.routes.append(route)
def mount(self, route, app):
"""Mounts ASGI / WSGI applications at a given route
"""
self.apps.update(route, app)
"""Mounts ASGI / WSGI applications at a given route"""
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):
if websocket:
@@ -267,7 +298,6 @@ class Router:
self.before_requests.setdefault("http", []).append(endpoint)
def url_for(self, endpoint, **params):
# TODO: Check for params
for route in self.routes:
if endpoint in (route.endpoint, route.endpoint.__name__):
return route.url(**params)
@@ -276,13 +306,13 @@ class Router:
async def default_response(self, scope, receive, send):
if scope["type"] == "websocket":
websocket_close = WebSocketClose()
await websocket_close(receive, send)
await websocket_close(scope, receive, send)
return
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):
for route in self.routes:
@@ -292,16 +322,60 @@ class Router:
return route
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):
assert scope["type"] in ("http", "websocket", "lifespan")
if scope["type"] == "lifespan":
await self.lifespan_handler(scope, receive, send)
await self.lifespan(scope, receive, send)
return
path = scope["path"]
root_path = scope.get("root_path", "")
# Check "primary" mounted routes first (before submounted apps)
route = self._resolve_route(scope)
scope["before_requests"] = self.before_requests
if route is not None:
await route(scope, receive, send)
return
# Call into a submounted app, if one exists.
for path_prefix, app in self.apps.items():
if path.startswith(path_prefix):
@@ -311,16 +385,10 @@ class Router:
await app(scope, receive, send)
return
except TypeError:
from a2wsgi import WSGIMiddleware
app = WSGIMiddleware(app)
await app(scope, receive, send)
return
route = self._resolve_route(scope)
scope["before_requests"] = self.before_requests
if route is not None:
await route(scope, receive, send)
return
await self.default_response(scope, receive, send)
await self.default_endpoint(scope, receive, send)
+3 -13
View File
@@ -1,18 +1,8 @@
import typing
from starlette.staticfiles import StaticFiles
from starlette.staticfiles import StaticFiles as StarletteStaticFiles
class StaticFiles(StaticFiles):
"""I've created an issue to disccuss allowing multiple directories in starletter's `StaticFiles`.
https://github.com/encode/starlette/issues/625
I've also made a PR to add this method to starlette StaticFiles
Once accepted we will remove this.
https://github.com/encode/starlette/pull/626
"""
class StaticFiles(StarletteStaticFiles):
"""Extension to Starlette's StaticFiles with support for multiple directories."""
def add_directory(self, directory: str) -> None:
self.all_directories = [*self.all_directories, *self.get_directories(directory)]
+3 -2
View File
@@ -1,7 +1,8 @@
API_THEMES = ["elements", "rapidoc", "redoc", "swagger_ui"]
DEFAULT_ENCODING = "utf-8"
DEFAULT_API_THEME = "swaggerui"
DEFAULT_OPENAPI_THEME = "swagger_ui"
DEFAULT_SESSION_COOKIE = "Responder-Session"
DEFAULT_SECRET_KEY = "NOTASECRET"
DEFAULT_SECRET_KEY = "NOTASECRET" # noqa: S105
DEFAULT_CORS_PARAMS = {
"allow_origins": (),
+1 -7
View File
@@ -1,5 +1,3 @@
# from: https://github.com/requests/requests/blob/master/requests/status_codes.py
codes = {
# Informational.
100: ("continue",),
@@ -26,11 +24,7 @@ codes = {
305: ("use_proxy",),
306: ("switch_proxy",),
307: ("temporary_redirect", "temporary_moved", "temporary"),
308: (
"permanent_redirect",
"resume_incomplete",
"resume",
), # These 2 to be removed in 3.0
308: ("permanent_redirect",),
# Client Error.
400: ("bad_request", "bad"),
401: ("unauthorized",),
+10 -4
View File
@@ -2,12 +2,18 @@ from contextlib import contextmanager
import jinja2
__all__ = ["Templates"]
class Templates:
def __init__(self, directory="templates", autoescape=True, context=None):
def __init__(
self, directory="templates", autoescape=True, context=None, enable_async=False
):
self.directory = directory
self._env = jinja2.Environment(
loader=jinja2.FileSystemLoader([str(self.directory)]), autoescape=autoescape
loader=jinja2.FileSystemLoader([str(self.directory)]),
autoescape=autoescape, # noqa: S701
enable_async=enable_async,
)
self.default_context = {} if context is None else {**context}
self._env.globals.update(self.default_context)
@@ -29,7 +35,7 @@ class Templates:
:param template: The filename of the jinja2 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)
@contextmanager
@@ -50,6 +56,6 @@ class Templates:
:param source: The template to use.
:param *args, **kwargs: Data to pass into the template.
:param **kwargs: Data to pass into the template.
"""
""" # noqa: E501
template = self._env.from_string(source)
return template.render(*args, **kwargs)
View File
+242
View File
@@ -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
+44
View File
@@ -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