Compare commits

..

376 Commits

Author SHA1 Message Date
Andreas Motl cc0fe78382 Packaging: Use versioningit for maintaining the package version
The Debian package builder has been saved to `bin/mkdeb.py` for now. It
needs to be refurbished into a `poethepoet` task subsequently.
2024-10-25 14:21:22 +02: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
taoufik 906cd2fbbf Fix templates conflicts 2019-10-20 12:48:02 +01:00
taoufik 9d0129da56 Fix templates conflicts 2019-10-20 12:39:32 +01:00
taoufik aedcf12d99 v2.0.1 2019-10-20 12:20:54 +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
Taoufik a7110ef441 Merge pull request #395 from vbarbaresi/master
add a few tests for API and remove some dead code
2019-10-19 13:06:01 +01:00
Vincent Barbaresi d3e4968546 add a few tests for API and remove some dead code
Increasing the coverage of api.py to 86%

- use pytest.parametrize to test cors and hsts parameters usage
- use pytest.parametrize to factorize test_staticfiles_custom_route
- add tests for path_matches_route and Route() with no endpoint
- test Jinja template rendering from a file

- remove   _notfound_wsgi_app, before_ws_requests, before_http_requests

they didn't seem used anywhere and the before_* method were broken, referring to
self.before_requests that doesn't exist anymore
2019-10-19 13:47:39 +02:00
Taoufik 03e34d56ab Merge pull request #398 from taoufik07/v2
v2.0.0
2019-10-19 12:22:32 +01:00
taoufik b470d10416 v2.0.0 2019-10-19 12:21:06 +01:00
Taoufik b8aea89039 Merge pull request #397 from taoufik07/update_pipfile
Bump versions
2019-10-19 12:13:06 +01:00
taoufik 4a1e89af1b Bump versions 2019-10-19 12:07:31 +01:00
Taoufik 81fbc94d36 Merge pull request #396 from vbarbaresi/fix_3.8_tests
upgrade pluggy to 0.13.0 to support importlib.metadata on python 3.8
2019-10-03 17:24:53 +01:00
Vincent Barbaresi 6487671559 upgrade pluggy to 0.13.0 to support stb lib importlib.metadata on python 3.8 2019-10-03 18:15:09 +02:00
Taoufik 838c7f29b5 Update docs url 2019-09-26 12:07:59 +01:00
Taoufik df85a4c214 Merge pull request #388 from taoufik07/new-router
New Router and refactor
2019-09-03 20:13:46 +02:00
taoufik 1bdbea238e Add a api.static_app and extend starlette's StaticFiles app 2019-08-31 01:38:00 +02:00
Taoufik 97dbef92d9 Bump werkzug version to 0.15.5 in docs 2019-08-21 23:05:27 +02:00
taoufik 85c1c0036c [WIP] Update docs 2019-08-21 22:50:58 +02:00
taoufik 0653ee2c6b Add Templates 2019-08-21 13:46:31 +02:00
taoufik d1db913c7d Bump dependencies 2019-08-17 22:15:43 +02:00
taoufik d24b921cdc Move open api schema to ext/schema 2019-08-17 18:25:19 +02:00
taoufik b31b742787 Implement a new Router and other changes
- Router
- Use starlette's Session middlewre
- Add Exception Middleware
- ...
2019-08-17 14:41:11 +02:00
Taoufik d820f0277f Merge pull request #387 from taoufik07/v-1.3.2
Bump version and update CHANGELOG
2019-08-15 22:57:21 +02:00
taoufik 7219856177 Bump version and update CHANGELOG 2019-08-15 22:51:28 +02:00
Taoufik e6d302aabb Merge pull request #385 from FirstKlaas/state-for-request
Added a state property to the request object.
2019-08-07 19:28:13 +02:00
FirstKlaas d73243ab60 Linting with black 2019-08-07 18:44:43 +02:00
FirstKlaas 8101e7d7b0 Added a state property to the request opbject. 2019-08-07 18:29:03 +02:00
Taoufik 784c7e72ae Merge pull request #381 from taoufik07/ci-py-3.8-dev
Travis-ci add python 3.8-dev
2019-08-05 20:08:44 +02:00
taoufik 93156fd2f1 Travis-ci add python 3.8-dev 2019-08-05 20:00:14 +02:00
Taoufik e4f6898498 Merge pull request #380 from taoufik07/refactor-changelog
Refactor CHANGELOG following the keepachangelog format
2019-08-05 19:59:00 +02:00
taoufik ef330135f9 Refactor CHANGELOG following the keepachangelog format
- Format following https://keepachangelog.com/en/1.0.0/
- Refactor CHANGELOGS
- Add missing changes
2019-08-05 19:56:01 +02:00
Taoufik 54cbbdface Merge pull request #379 from taoufik07/asgi3
ASGI 3 support
2019-08-05 19:38:46 +02:00
taoufik f3c9320837 ASGI 3 support 2019-08-05 15:33:56 +02:00
Taoufik 0529629ac8 Merge pull request #378 from ucpr/add_formats_test
add test for #367
2019-07-29 23:00:50 +02:00
Taoufik 22af42ead6 Merge pull request #377 from ucpr/fix_readme
fix links
2019-07-29 22:59:40 +02:00
ucpr bd2efb68e1 test: add test for #367 2019-07-30 00:26:01 +09:00
ucpr 37ba3d2efc fix: fix links 2019-07-29 23:24:57 +09:00
kennethreitz ed8afeaa87 Merge pull request #367 from ucpr/fix_format_form
format data when mimetype is form-data
2019-07-16 11:05:57 -04:00
kennethreitz ee6efe5aa4 Merge pull request #372 from mlschneid/master
Encouraging use of stable
2019-07-16 11:05:07 -04:00
Mike Schneider 3a8113d8b0 Found more --pre flags 2019-07-16 07:43:55 -07:00
Mike Schneider 7afce42943 Encouraging use of stable 2019-07-16 07:34:35 -07:00
Frost Ming 67a6c25256 Merge pull request #370 from waghanza/remove_pipenv_requires
Specify constraint for python version in pipenv
2019-07-09 17:15:53 +08:00
Marwan Rabbâa 38dea8311c refactor(pip): Remove pipenv restriction on python version 2019-07-09 11:09:29 +02:00
Marwan Rabbâa 555e1f7924 refactor(pip): Specify constraint for python version in pipenv 2019-07-08 18:35:42 +02:00
Taoufik 8b87f63609 Merge pull request #369 from waghanza/py_37
Add python3.7 on CI
2019-07-08 13:56:46 +02:00
Marwan Rabbâa f01b1d493f feat(ci): Test under python 3.7 2019-07-08 09:26:34 +02:00
ucpr a802245bf0 format data when mimetype is form-data 2019-07-01 05:58:43 +09:00
kennethreitz 6dbbad158a Merge pull request #357 from amikrop/master
Remove extra `static_url` function in `API.docs`
2019-06-25 09:17:14 -07:00
kennethreitz 877fe144b4 Merge pull request #365 from tedder/ted/add_links_2
add some links to the main page
2019-06-25 09:16:47 -07:00
ted 70e6bc0466 add some links to the main page
Basically linked things I had to google/look up on my own. There are a million things that _could_ be linked, but using myself as a median Python coder, it seems about right. Removed italics because I can't figure out how to use them with a link.
2019-06-25 08:21:07 -07:00
Taoufik 63f2e833eb Merge pull request #362 from mtcronin99/patch-1
Update tour.rst to add quotes around hostnames
2019-06-04 17:29:35 +02:00
mtcronin99 fb71abe534 Update tour.rst to add quotes around hostnames
The API call in the Trusted Hosts section needs quotes around the hostnames in the example.
2019-06-04 11:21:27 -04:00
Taoufik 8ccb39560e Merge pull request #359 from EdwardBetts/patch-1
Correct a spelling mistake
2019-05-11 22:24:16 +02:00
Edward Betts e6b880be62 Correct a spelling mistake 2019-05-11 20:17:10 +01:00
kennethreitz d0016ac7c9 Update README.md 2019-05-06 07:42:31 -04:00
Aristotelis Mikropoulos 05035e0171 Remove extra static_url function in API.docs 2019-05-02 17:09:34 +03:00
Taoufik 78b5bef879 Merge pull request #354 from kennethreitz/starlette-new-lifespan
Starlette 0.11.* and cleanup
2019-04-29 23:14:27 +02:00
taoufik a6955b5db5 Fix lifespan and bump starlette to 0.11.* 2019-04-29 23:09:48 +02:00
taoufik a1a0a1b71e Remove duplicated code and rename _dispatch_request 2019-04-29 22:44:44 +02:00
Taoufik 0bdde6d5fe Merge pull request #353 from kennethreitz/taoufik07-patch-1
Typo schema docs
2019-04-28 16:02:29 +02:00
Taoufik cf5447d5bd Typo schema docs 2019-04-28 15:59:15 +02:00
taoufik b2dd2c205d Fix tests 2019-04-28 15:48:36 +02:00
kennethreitz e52c9277c8 fix lockfile 2019-04-28 09:43:29 -04:00
kennethreitz 712ec2410d changes 2019-04-28 09:39:12 -04:00
kennethreitz dea2ca41d2 Merge pull request #351 from kennethreitz/route-params-docs
Add docs for route params convertors
2019-04-28 09:19:42 -04:00
Taoufik ca0f32c02b Black check before tests 2019-04-27 20:44:02 +02:00
Taoufik f21b296fba Add docs for route params convertors 2019-04-27 20:10:31 +02:00
Taoufik 3224479b99 Merge pull request #350 from kennethreitz/cleanup
Cleanup
2019-04-27 13:52:48 +02:00
Taoufik f95950eedc Cleanup 2019-04-27 13:49:54 +02:00
kennethreitz 4467376d0a Merge pull request #333 from taoufik07/custom_specifiers
Route params convertors
2019-04-27 07:32:13 -04:00
kennethreitz ac65dc5361 Merge pull request #347 from s-pace/doc/add_search
[doc website] Add a nice search experience
2019-04-27 07:25:38 -04:00
s-pace 4957793c80 feat: add search to every documentation pages 2019-04-22 22:23:07 +02:00
s-pace ff7f4b502d feat: add search to the main introduction page 2019-04-22 22:22:46 +02:00
Timo Furrer 816cb7188b Merge pull request #339 from jonbeebe/master
Removed asgiref dependency
2019-03-29 18:34:14 +01:00
Jonathan Beebe 6456d435eb Revert Pipfile.lock (with asgiref removed) 2019-03-28 20:36:11 -07:00
Jonathan Beebe 63e338ed6f Removed asgiref dependency
Replaced WsgiToAsgi (asgiref.wsgi) usage with WSGIMiddleware (starlette.middleware.wsgi) to fix breaking changes with asgiref 3.0.0.
2019-03-28 20:12:22 -07:00
Timo Furrer 00211c8f03 Merge pull request #335 from kobayashi/master
add sample of uploading a file
2019-03-21 21:30:14 +01:00
kobayashi ebed9fe3aa fix typos 2019-03-17 19:05:14 -04:00
kobayashi 734b5e7303 add sample of uploading a file 2019-03-17 15:21:27 -04:00
Taoufik 1696d501e2 Merge pull request #334 from MerleLiuKun/fix_test_responder_variable
fix variable error at test responder
2019-03-14 08:19:53 +00:00
ikronskun e65d2f8c50 fix variable error at test responder 2019-03-14 14:25:45 +08:00
taoufik07 9ea705b2ea Specifiers test 2019-03-12 17:39:53 +01:00
taoufik07 5a5a811dca Add routes specifiers 2019-03-12 16:58:36 +01:00
kennethreitz df7b9419c2 Merge pull request #332 from jlewis91/patch-1
Update tour.rst to be openapi compliant
2019-03-10 10:55:38 -04:00
Jeremiah 37318f1106 Update tour.rst 2019-03-10 14:53:44 +01:00
kennethreitz 19e9f6ac5d Merge pull request #320 from taoufik07/static_serve
Fix #303
2019-03-05 08:33:39 -05:00
kennethreitz 658b51a449 Merge pull request #321 from taoufik07/revert_before_requests
Revert before_requests
2019-03-05 08:33:25 -05:00
Parth Shandilya 485303c0f2 Merge pull request #314 from vlcinsky/fix_uvloop_env_marker
Fix #313 incomplete environment marker for uvloop
2019-03-03 23:22:53 +05:30
taoufik07 885d902b7d Revert 2019-02-26 23:46:07 +01:00
taoufik07 a35f02fb64 Add tests 2019-02-26 22:23:43 +01:00
taoufik07 28d1f16ad5 Disable serving when static_dir is None and handle templates_dir 2019-02-26 22:10:13 +01:00
Taoufik a04d7c3a9a Merge pull request #319 from taoufik07/refactor_before_ws
Refactor before_requests and websockets
2019-02-26 19:27:26 +00:00
taoufik07 b876f8484c Update docs 2019-02-26 17:01:13 +01:00
taoufik07 854c6d3d65 Add @before_request 2019-02-26 16:44:12 +01:00
taoufik07 f9a850a8fe Add before_requets for ws and refactor 2019-02-26 15:50:55 +01:00
taoufik07 e808662fe7 Refactor websockets 2019-02-26 15:27:26 +01:00
Taoufik 7bbb02126e Merge pull request #316 from tkamenoko/patch-4
fix typo
2019-02-23 15:15:31 +00:00
Taoufik aa101059a7 Merge pull request #315 from taoufik07/websockets_tests
Websockets tests
2019-02-23 15:11:23 +00:00
T.Kameyama d1f7fe02e4 fix typo 2019-02-24 00:10:47 +09:00
taoufik07 3e26dc1373 Add websockets tests 2019-02-23 16:00:22 +01:00
Jan Vlčinský 0a9d819555 Merge branch 'master' into fix_uvloop_env_marker 2019-02-22 20:47:25 +01:00
Jan Vlcinsky b31dfeefb7 Fix #313 incomplete environment marker for uvloop 2019-02-22 20:44:32 +01:00
Taoufik fc640ec331 Merge pull request #312 from iancleary/bug/242_docs_consistency
fixed flow between OpenAPI and Interactive Docs sections
2019-02-22 13:33:37 +00:00
iancleary 3382723457 fixed flow between OpenAPI and Interactive Docs sections 2019-02-22 06:16:45 -07:00
Taoufik 1fc0722ad6 Merge pull request #310 from taoufik07/fix/req_text
DEFAULT_ENCODING if none is detected
2019-02-22 11:57:11 +00:00
taoufik07 b21e308357 Add tests 2019-02-22 12:44:32 +01:00
taoufik07 738105314b Return DEFAULT_ENCODING if none and remove redundant code 2019-02-22 12:34:22 +01:00
Timo Furrer f3cdc99b29 release: 1.3.0 2019-02-22 10:36:57 +00:00
Taoufik eb70376438 v1.3.0 changelog 2019-02-22 10:34:19 +00:00
Timo Furrer dae1a4fa35 Add template for 1.3.0 CHANGELOG 2019-02-22 10:16:17 +00:00
Taoufik 2ad351197e Merge pull request #304 from taoufik07/content_type
Add resp.html property and make resp.text a property
2019-02-22 10:12:46 +00:00
Taoufik 3d9235c4bc Merge pull request #293 from taoufik07/multiple_cookies_and_directives
Multiple cookies and directives
2019-02-22 10:12:18 +00:00
taoufik07 2cd5596def lint 2019-02-22 10:40:27 +01:00
Taoufik d4191030d9 Merge branch 'master' into multiple_cookies_and_directives 2019-02-22 10:33:53 +01:00
Taoufik 447630a051 Merge branch 'master' into content_type 2019-02-22 10:30:38 +01:00
Timo Furrer f7b53a4895 Merge pull request #308 from taoufik07/feature/stream
Support stream response
2019-02-22 10:15:42 +01:00
taoufik07 21896aa171 Support stream response 2019-02-22 03:58:09 +01:00
Taoufik e8a15697d2 Merge pull request #306 from iancleary/242_modify_swagger_strings
implemented rest of OpenAPI Info Object
2019-02-22 02:05:23 +00:00
icleary 0030993631 api OpenAPI params match /docs display order, updated tour docs and docs test 2019-02-21 18:35:19 -07:00
taoufik07 13ba2f72f5 Update docs 2019-02-22 02:00:12 +01:00
taoufik07 9f39917895 Update docs and README 2019-02-22 01:12:25 +01:00
taoufik07 1b0859fdbb Only encode text 2019-02-22 00:59:17 +01:00
taoufik07 acd1561b1b Add tests 2019-02-22 00:01:49 +01:00
icleary 9f2182949d snake case for terms_of_service, is not None for if statements 2019-02-21 08:35:53 -07:00
icleary 6e5b3a4bf9 ran black on changed file 2019-02-20 22:38:57 -07:00
icleary d2ec323888 edited docstring to remove ->type 2019-02-20 22:29:49 -07:00
icleary 8b9645cf2d implemented rest of OpenAPI Info Object 2019-02-20 22:04:58 -07:00
Taoufik 4ecfef0ddf Merge branch 'master' into content_type 2019-02-21 02:57:06 +01:00
taoufik07 84fb7bd622 resp.html 2019-02-21 02:55:20 +01:00
taoufik07 0b261252e1 Add resp.html property and make resp.text a property 2019-02-21 02:49:47 +01:00
Taoufik d60b5ee39e Merge pull request #297 from taoufik07/whitnoise_notfound 2019-02-21 00:19:02 +00:00
taoufik07 e2f887ec5f Add tests 2019-02-21 00:51:31 +01:00
taoufik07 97da6a6694 Format static_route 2019-02-21 00:46:27 +01:00
taoufik07 c0e9a6778d Set static_response status if not found 2019-02-20 23:20:37 +01:00
Taoufik 5c327a2e0b Merge pull request #302 from taoufik07/lock_update
Pin starlette to 0.10.* and update the lock file
2019-02-20 17:52:00 +00:00
taoufik07 5ed45634cb Pin starlette to 0.10.* and update the lock file 2019-02-20 18:45:53 +01:00
Taoufik a50a373e84 Merge pull request #301 from taoufik07/starlette-0.10.5 2019-02-19 21:59:27 +00:00
taoufik07 86705d0c2f Pin starlette to 0.10.5 2019-02-19 22:35:23 +01:00
taoufik07 b9581444f9 Return 404 when static file is not found 2019-02-19 14:47:31 +01:00
Taoufik 2a60b094b8 Merge branch 'master' into multiple_cookies_and_directives 2019-02-19 13:49:21 +01:00
taoufik07 1ec567cabf Cleanup and black 2019-02-19 13:47:59 +01:00
Taoufik 4fd898b239 Merge pull request #296 from taoufik07/travis_black_check 2019-02-19 12:43:01 +00:00
taoufik07 03d6b72a00 Add linting checks to travis 2019-02-19 13:34:02 +01:00
taoufik07 4d0382d580 Lint 2019-02-19 13:33:17 +01:00
taoufik07 a0dd7481ec Add tests 2019-02-17 19:39:36 +01:00
taoufik07 1c91480b0c Multiple cookies and directives 2019-02-17 19:35:34 +01:00
Taoufik 85e5ec0a9a Merge pull request #288 from taoufik07/starlette>=0.10.2
Update Pipfile.lock and starlette==0.10.*
2019-02-14 13:49:57 +00:00
Taoufik 4ac04b0abc Merge pull request #290 from josegonzalez/patch-1 2019-02-14 13:49:07 +00:00
Taoufik d7e64a6e39 Merge pull request #289 from taoufik07/patch-20 2019-02-14 12:39:40 +00:00
Taoufik 17d526632e Merge pull request #285 from taoufik07/patch-19
Await for background task
2019-02-14 12:34:21 +00:00
Jose Diaz-Gonzalez 43da481df7 fix: always respect the configured session_cookie
The `session_cookie` was refactored to be set via a hardcoded `DEFAULT_SESSION_COOKIE` static variable, and this change will allow future cookie changes to trickle to the Request object.
2019-02-13 10:54:07 -05:00
Taoufik 5f5402833b Typos 2019-02-13 15:09:48 +00:00
taoufik07 d59c4333f2 starlette 0.10.* 2019-02-13 16:03:20 +01:00
taoufik07 49114f36ce Update starlette>=0.10.2 2019-02-13 12:14:13 +01:00
taoufik07 b2039d99f3 Update Pipfile.lock 2019-02-13 12:13:16 +01:00
Taoufik 94fd86fee0 Await for background task 2019-02-11 08:45:48 +00:00
kennethreitz d70fdd3301 todo 2019-02-09 06:45:25 -06:00
kennethreitz 05b75efb43 version 2019-01-12 07:07:50 -05:00
kennethreitz be56e92d65 Merge pull request #277 from amikrop/master
Make `_route_for` shorter
2019-01-12 07:06:31 -05:00
kennethreitz 69eb843604 fixes 2019-01-12 06:57:28 -05:00
Aristotelis Mikropoulos 84a7f0e90b Make _route_for shorter 2019-01-11 03:46:35 +02:00
kennethreitz d1e105a29a Merge pull request #276 from tomchristie/resolve-startup-and-shutdown-events
Resolve startup/shutdown events
2019-01-09 17:49:49 -05:00
Tom Christie 9f0a568fa3 Resolve startup/shutdwown events 2019-01-09 12:42:15 +00:00
kennethreitz 05b46cbe34 Merge pull request #269 from erm/lifespan-handler
Return lifespan handler in dispatch method
2018-12-29 07:05:41 -05:00
kennethreitz c045586997 Merge pull request #267 from valtyr/graphql-context
Add Request and Response to GraphQL context
2018-12-29 07:05:26 -05:00
kennethreitz 8f0707f697 Merge pull request #262 from carlodri/carlodri-patch-1
include LICENSE il sdist
2018-12-29 07:05:12 -05:00
kennethreitz 36929b265c Merge pull request #263 from tkamenoko/patch-3
fix docstring of `API.url_for`
2018-12-29 07:05:05 -05:00
kennethreitz 734ba64965 Merge pull request #264 from sangheestyle/master
fix typo
2018-12-29 07:04:53 -05:00
kennethreitz 148e6742df Merge pull request #268 from taoufik07/patch-18
Websocket docs
2018-12-29 07:04:45 -05:00
Jordan bcb7e8f4f3 Update lock file 2018-12-28 18:10:27 +11:00
Jordan f678112099 Return lifespan handler in dispatch method 2018-12-28 17:46:27 +11:00
Taoufik 60b0c5f256 Update 2018-12-25 01:32:38 +00:00
Taoufik c8627939de Update 2018-12-25 01:18:44 +00:00
Taoufik 9144f0158a Websocket docs 2018-12-25 01:15:54 +00:00
Valtýr Örn Kjartansson d541aca80f Mention the GraphQL context object in docs 2018-12-23 13:28:50 +00:00
Valtýr Örn Kjartansson c73b2b8d34 Add request and response to GraphQL context
This allows the GraphQL resolvers access to things like session, headers etc.
Also enables creation of powerful GraphQL middleware.
2018-12-23 13:14:15 +00:00
Kim, Sanghee e2493b489d fix typo 2018-12-20 17:54:15 +02:00
T.Kameyama 8dee28ac7c fix docstring of API.url_for 2018-12-17 19:23:33 +09:00
Carlo cdd3885a0c include LICENSE il sdist 2018-12-12 22:35:41 +01:00
kennethreitz 1a28d528d0 Merge pull request #233 from taoufik07/websocket-x.x
WebSocket returns
2018-12-12 03:59:24 -05:00
kennethreitz 3ba12b8cee Merge pull request #230 from tkamenoko/file-form
formats: format_file returns filename, data, and content-type [Breaking]
2018-12-12 03:59:10 -05:00
kennethreitz 5a29ab6917 Merge pull request #257 from ibnesayeed/lifespan
Import lifespan as a middleware as per the change in starlette package
2018-12-12 03:58:53 -05:00
kennethreitz 694144a0c8 Merge pull request #244 from barrust/route-isclass-fix
fix for route.is_class_based
2018-12-12 03:58:41 -05:00
kennethreitz 8bed8e8741 Merge pull request #249 from abstiles/fix-broken-test-500
Fix broken exception handling in test_500
2018-12-12 03:58:06 -05:00
kennethreitz a81a348bce Merge pull request #245 from cdfuller/add-monospace-default
Add monospace fallback for preformatted text in docs
2018-12-12 03:57:55 -05:00
kennethreitz fd9e8c5cbc Merge pull request #240 from barrust/persist-debug
persist debug from API to run
2018-12-12 03:57:19 -05:00
kennethreitz 8030b1919d Merge pull request #253 from TomFaulkner/patch-1
Grammatical fix
2018-12-12 03:56:52 -05:00
Sawood Alam 72c789fdd7 Update Pipfile.lock to reflect version changes 2018-11-30 12:30:28 -05:00
Sawood Alam 1113a9aa0d Import lifespan as a middleware as per the change in starlette package 2018-11-30 11:52:43 -05:00
Tom Faulkner a5532614a2 Grammatical fix
Grammar: `into to` to `into`
2018-11-26 19:30:20 -06:00
Andrew Stiles 122023fb70 Restore the removal of ExceptionMiddleware
Back out the changes made to API and fix the test with a non-raising
instance of TestClient.
2018-11-21 13:05:51 -06:00
Andrew Stiles b8fa923ec9 Fix broken exception handling in test_500 2018-11-21 11:30:31 -06:00
Tyler Barrus ae06b3e01a default cont to False 2018-11-20 10:38:15 -05:00
Cody Fuller 5599ec2809 Add monospace fallback for preformatted text 2018-11-20 09:37:30 -06:00
Tyler Barrus e795cbddb6 fix for route.is_class_based 2018-11-20 10:26:29 -05:00
Tyler Barrus 0cb087c37b persist debug from API to run 2018-11-19 23:37:04 -05:00
taoufik07 983cbcc711 cleanup 2018-11-17 21:40:17 +01:00
taoufik07 6d154b0c78 WIP websocket 2018-11-17 20:49:26 +01:00
Tessei Kameyama f3f36e28c4 [formats] format-files detects file or simple-data
if `part` has `content-type`, `format_files` returns
`{"name" : filedata}` . `filedata` is `dict`, the keys are
`filename, content, content-type` .

Otherwise, `format_files` just returns `{"name" : b"data"}` .
2018-11-16 23:50:00 +09:00
Tessei Kameyama fdf4797726 [test] update file-upload [wip]
if form-data don't have content-type,
`req.media("files")` returns `{"name" : b"data"}` , else
`{"name" :
{"filename" : "foo.txt",
"content" : b"data",
"content-type" : "some/type"} }` .

not implemented yet.
2018-11-16 22:13:05 +09:00
Tessei Kameyama 67d8a3be98 [fix] typo 2018-11-16 20:23:09 +09:00
kennethreitz 4001a60f6c Merge pull request #229 from tomchristie/run-in-threadpool
Use Starlette's run_in_threadpool
2018-11-15 06:24:34 -05:00
kennethreitz d94db41271 Merge branch 'master' of https://github.com/kennethreitz/responder 2018-11-15 06:25:42 -05:00
kennethreitz 8abb78bb58 fix for #215 2018-11-15 06:22:04 -05:00
kennethreitz a80db99aa3 Merge pull request #228 from mmanhertz/patch-2
Fix issue in OpenAPI Schema Support example
2018-11-15 05:49:07 -05:00
Tom Christie 69a300f21a Use Starlette's run_in_threadpool 2018-11-15 10:38:05 +00:00
Matthias Manhertz 1b024b8092 Fix issue in OpenAPI Schema Support example
`PetSchema().dump()` returns a dict. Calling `.data` on it causes an AttributeError.
2018-11-15 11:19:11 +01:00
kennethreitz a622689597 Merge pull request #225 from peerster/patch-1
Update README
2018-11-14 06:18:13 -05:00
Pär 9943e66c49 Updates all of the python-responder.org links 2018-11-14 11:10:29 +01:00
Pär 7233c08281 Update README
Update link to python-responder.org to use https
2018-11-14 11:07:18 +01:00
kennethreitz 0845d92fda Merge pull request #223 from jkermes/master
Fix static response
2018-11-13 09:33:34 -05:00
Julien Kermes 1cc02e5a83 Fix static response 2018-11-13 15:18:30 +01:00
kennethreitz aa4cd7a144 Merge pull request #222 from MRSharff/debian-manifest-typofix
Fixed a typo in setup.py: mainfest -> manifest
2018-11-13 07:10:34 -05:00
Mathew Sharff b42ae0dfd7 Fixed a typo in setup.py: mainfest -> manifest 2018-11-12 21:33:12 -08:00
kennethreitz a6bd179726 version bump 2018-11-11 16:58:07 -05:00
kennethreitz aac7b5117b Merge pull request #218 from tomchristie/patch-1
Bring testominials in line with README
2018-11-10 17:48:29 -05:00
Tom Christie 8e8e99ed2e Bring testominials in line with README 2018-11-09 18:25:53 +00:00
kennethreitz 078ac23b20 Merge pull request #213 from mmanhertz/fix-212
fix for #212
2018-11-07 17:25:14 -05:00
kennethreitz 8e61df6b6a Merge pull request #207 from taoufik07/await_backgroundtask_raise
Await for background task and raise
2018-11-07 17:25:02 -05:00
mmanhertz cf7fb56653 fix for #212 2018-11-07 17:05:18 +01:00
taoufik07 da20d13c49 Await for background task and raise 2018-11-06 17:27:04 +01:00
kennethreitz 7a250aa8fc Merge branch 'master' of github.com:kennethreitz/responder 2018-11-06 05:46:25 -05:00
kennethreitz af28ecb82d fix for #200 2018-11-06 05:46:17 -05:00
kennethreitz 39a0e52a2a Merge pull request #201 from mmanhertz/patch-1
Fix OpenAPI version in examples
2018-11-06 05:12:51 -05:00
Matthias Manhertz a4f5a111c7 Fix OpenAPI version in examples
The example for "OpenAPI Schema Support" and "Interactive Documentation had `openapi="3.0"` which would cause the following error when swagger UI tried to render the schema:
```
Unable to render this definition
The provided definition does not specify a valid version field.

Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match  openapi: 3.0.n (for example, openapi: 3.0.0).
```
2018-11-06 10:46:30 +01:00
kennethreitz c65e585493 Merge pull request #192 from vlcinsky/fix_doc_typo
Fixed missing `.data` on Marshmallow dump
2018-11-05 17:40:06 -05:00
kennethreitz c9e94561aa Merge pull request #196 from xyb/patch-1
fix Dockerfile
2018-11-05 14:09:13 -05:00
Xie Yanbo 1daa4c202b fix Dockerfile 2018-11-06 00:47:09 +08:00
kennethreitz 83ff361672 Merge pull request #195 from taoufik07/fix/routes_conflict
Fix/routes conflict
2018-11-05 09:01:15 -05:00
taoufik07 2c686f107d Tests 2018-11-05 14:52:08 +01:00
taoufik07 ac8ec3d5ef cleanup 2018-11-05 14:51:56 +01:00
taoufik07 21e70ef913 Fix tests 2018-11-05 14:48:34 +01:00
taoufik07 c4f5b0e7c2 Fix routes conflict 2018-11-05 14:47:46 +01:00
Jan Vlcinsky 45d6c1389d Fixed missing .data on Marshmallow dump 2018-11-04 22:06:54 +01:00
kennethreitz 0c9bc5a3af fix 2018-11-04 07:22:03 -05:00
kennethreitz 5b67d5a04a Merge branch 'master' of github.com:kennethreitz/responder 2018-11-04 07:20:41 -05:00
kennethreitz f3396a5573 fix for #188 2018-11-04 07:20:30 -05:00
kennethreitz e9f48788a3 Merge pull request #184 from rparrapy/change-route-cache
Replace memoize decorator by functools.lru_cache
2018-11-02 07:09:08 -04:00
Rodrigo Parra 6993a1ea46 Replace memoize decorator by functools.lru_cache 2018-11-02 06:32:01 -03:00
kennethreitz 8bcfb4585b cleanup testimonials 2018-11-01 05:38:22 -04:00
kennethreitz db45251f7f Merge pull request #182 from rparrapy/patch-1
Fix typo
2018-10-31 17:39:02 -04:00
kennethreitz 5582667b4f deployment 2018-10-31 17:37:13 -04:00
Rodrigo Parra 2c898aaf23 Fix typo 2018-10-31 14:36:40 -03:00
kennethreitz e0999ffcdd Merge branch 'master' of github.com:kennethreitz/responder 2018-10-31 06:10:49 -04:00
kennethreitz 03811768bb depoloyment 2018-10-31 06:10:40 -04:00
kennethreitz fbef577c9f Merge pull request #179 from taoufik07/patch-16
Typo :3
2018-10-30 21:56:18 -04:00
Taoufik 9434510ce9 Typo :3 2018-10-31 01:04:25 +01:00
kennethreitz 354130c151 .serve 2018-10-30 19:08:57 -04:00
kennethreitz 3e3cba016a Merge pull request #177 from tomchristie/patch-1
Update api.py
2018-10-30 10:37:38 -04:00
Tom Christie f75e120bef Update api.py
Refs https://github.com/kennethreitz/responder/issues/174

You don't need both to use both DebugMiddleware and ExceptionMiddleware, just one or the other.

Currently errors will cause a traceback 500 page, then re-raise, then attempt to generate another traceback 500 page, resulting in the behavior commented on in #174
2018-10-30 14:34:10 +00:00
kennethreitz 1d0294e430 merge 2018-10-30 05:01:25 -04:00
kennethreitz f786dd8254 apispec 2018-10-30 04:59:24 -04:00
kennethreitz cd9d09fd53 Merge pull request #175 from taoufik07/patch-15
Set debug to False in tests
2018-10-29 19:04:32 -04:00
kennethreitz 7471bbcd4e Merge pull request #173 from taoufik07/patch-14
Trusted hosts docs
2018-10-29 19:04:19 -04:00
Taoufik 43b04eccbd Set debug to False in tests 2018-10-29 22:53:56 +01:00
Taoufik 6a5d0b5e9f Trusted hosts docs 2018-10-29 21:56:54 +01:00
kennethreitz 359d366de4 Merge pull request #172 from taoufik07/trustedhost_starlette
starlette's TrustedhostMiddleware
2018-10-29 16:33:35 -04:00
taoufik07 556d9f3a7b Update starlette 2018-10-29 21:08:38 +01:00
taoufik07 2cab2dcec0 Tests 2018-10-29 20:54:38 +01:00
taoufik07 99d4e78dc9 Use starlette TrustedHostMiddleware 2018-10-29 20:48:51 +01:00
kennethreitz 9aa99869ae next version 2018-10-29 08:00:44 -04:00
kennethreitz 08e0d87347 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-29 07:57:19 -04:00
kennethreitz 3f9e4057d3 run sync tasks in threadpoolexecutor 2018-10-29 07:54:59 -04:00
kennethreitz a29e40353c Merge pull request #170 from taoufik07/trusted_hosts
Trusted hosts support
2018-10-28 14:47:28 -04:00
taoufik07 778cb2dd0f Add Tests 2018-10-28 18:26:42 +00:00
taoufik07 f7d5514b94 Fix test base_url 2018-10-28 18:12:21 +00:00
taoufik07 954637f7b3 Pass base_url to the TestClient 2018-10-28 18:09:52 +00:00
taoufik07 1ab46104c8 Allow all hosts by default 2018-10-28 14:51:24 +00:00
kennethreitz 815776d473 Merge branch 'master' of github.com:kennethreitz/responder 2018-10-28 05:25:56 -04:00
kennethreitz 8db1a7be90 Merge pull request #169 from taoufik07/patch-13
typo
2018-10-28 05:23:56 -04:00
taoufik07 7b11fa24dd Silence for now 2018-10-28 01:38:17 +01:00
taoufik07 1f0f2318d5 cleanup 2018-10-28 01:34:26 +01:00
taoufik07 029b3e2a52 Tests 2018-10-28 00:46:50 +01:00
taoufik07 4fff823def Trusted host 2018-10-28 00:46:39 +01:00
Taoufik cab78275f4 typo 2018-10-27 22:25:11 +01:00
93 changed files with 6931 additions and 2354 deletions
+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"
+41
View File
@@ -0,0 +1,41 @@
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 }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [
"ubuntu-latest",
"macos-12",
"macos-latest",
]
python-version: [
"3.10",
"3.11",
"3.12",
"3.13",
"pypy3.10",
]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: yezz123/setup-uv@v4
- run: uv pip install --editable '.[graphql,develop,test]' --system
- run: poe check
+1
View File
@@ -1,3 +1,4 @@
.venv*
.vscode/
.cache
.idea
-12
View File
@@ -1,12 +0,0 @@
language: python
python:
- "3.6"
# command to install dependencies
install:
- "pip install pipenv --upgrade-strategy=only-if-needed"
- "pipenv install --dev"
# command to run the dependencies
script:
- "pytest"
+324 -42
View File
@@ -1,99 +1,381 @@
# v1.1.0
# 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).
## [Unreleased]
## [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.4
## [v1.0.5]- 2018-10-27
### Fixed
- Fix sessions.
## [v1.0.4] - 2018-10-27
### Fixed
- Potential bufix for cookies.
# v1.0.3
## [v1.0.3] - 2018-10-27
### Fixed
- Bugfix for redirects.
# v1.0.2
## [v1.0.2] - 2018-10-27
### Changed
- Improvement for static file hosting.
# v1.0.1
## [v1.0.1] - 2018-10-26
### Changed
- Improve cors configuration settings.
# v1.0.0
## [v1.0.0] - 2018-10-26
### Changed
- Move GraphQL support into a built-in plugin.
# v0.3.3
- Improved exceptions.
- CORS support.
## [v0.3.3] - 2018-10-25
### Added
- CORS support
### Changed
- Improved exceptions.
## [v0.3.2] - 2018-10-25
### Changed
# v0.3.2
- Subtle improvements.
# v0.3.1
## [v0.3.1] - 2018-10-24
### Fixed
- Packaging fix.
# v0.3.0
## [v0.3.0] - 2018-10-24
### Changed
- Interactive Documentation endpoint.
- Minor improvements.
# v0.2.3
## [v0.2.3] - 2018-10-24
### Changed
- Overall improvements.
# v0.2.2
## [v0.2.2] - 2018-10-23
### Added
- Show traceback info when background tasks raise exceptions.
# v0.2.1
## [v0.2.1] - 2018-10-23
### Added
- api.requests.
# v0.2.0
## [v0.2.0] - 2018-10-22
### Added
- WebSocket support.
# v0.1.6
## [v0.1.6] - 2018-10-20
### Added
- 500 support.
# v0.1.5
- Improvements to sequential media reading.
- File upload support.
## [v0.1.5] - 2018-10-20
### Added
- File upload support
### Changed
- Improvements to sequential media reading.
## [v0.1.4] - 2018-10-19
### Fixed
# v0.1.4
- Stability.
# v0.1.3
## [v0.1.3] - 2018-10-18
### Added
- Sessions support.
# v0.1.2
## [v0.1.2] - 2018-10-18
### Added
- Cookies support.
# v0.1.1
## [v0.1.1] - 2018-10-17
### Changed
- Default routes.
# v0.1.0
## [v0.1.0] - 2018-10-17
### Added
- Prototype of static application support.
# v0.0.10
## [v0.0.10] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
# v0.0.9
## [v0.0.9] - 2018-10-17
### Fixed
- Bugfix for async class-based views.
# v0.0.8
## [v0.0.8] - 2018-10-17
### Added
- GraphiQL Support.
### Changed
- Improvement to route selection.
# v0.0.7
- Immutable Request object.
## [v0.0.7] - 2018-10-16
# v0.0.6:
- Ability to mount WSGI apps.
- Supply content-type when serving up the schema.
### Changed
# v0.0.5:
- OpenAPI Schema support.
- Safe load/dump yaml.
- Immutable Request object.
# v0.0.4:
- Asynchronous support for data uploads.
- Bug fixes.
## [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
# v0.0.3:
- Bug fixes.
# v0.0.2
## [v0.0.3] - 2018-10-13
### Fixed
- Bug fixes.
## [v0.0.2] - 2018-10-13
### Changed
- Switch to ASGI/Starlette.
# v0.0.1
## [v0.0.1] - 2018-10-12
### Added
- Conception!
[unreleased]: https://github.com/taoufik07/responder/compare/v2.0.5..HEAD
[v2.0.5]: https://github.com/taoufik07/responder/compare/v2.0.4..v2.0.5
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
[v1.2.0]: https://github.com/taoufik07/responder/compare/v1.1.3..v1.2.0
[v1.1.3]: https://github.com/taoufik07/responder/compare/v1.1.2..v1.1.3
[v1.1.2]: https://github.com/taoufik07/responder/compare/v1.1.1..v1.1.2
[v1.1.1]: https://github.com/taoufik07/responder/compare/v1.1.0..v1.1.1
[v1.1.0]: https://github.com/taoufik07/responder/compare/v1.0.5..v1.1.0
[v1.0.5]: https://github.com/taoufik07/responder/compare/v1.0.4..v1.0.5
[v1.0.4]: https://github.com/taoufik07/responder/compare/v1.0.3..v1.0.4
[v1.0.3]: https://github.com/taoufik07/responder/compare/v1.0.2..v1.0.3
[v1.0.2]: https://github.com/taoufik07/responder/compare/v1.0.1..v1.0.2
[v1.0.1]: https://github.com/taoufik07/responder/compare/v1.0.0..v1.0.1
[v1.0.0]: https://github.com/taoufik07/responder/compare/v0.3.3..v1.0.0
[v0.3.3]: https://github.com/taoufik07/responder/compare/v0.3.2..v0.3.3
[v0.3.2]: https://github.com/taoufik07/responder/compare/v0.3.1..v0.3.2
[v0.3.1]: https://github.com/taoufik07/responder/compare/v0.3.0..v0.3.1
[v0.3.0]: https://github.com/taoufik07/responder/compare/v0.2.3..v0.3.0
[v0.2.3]: https://github.com/taoufik07/responder/compare/v0.2.2..v0.2.3
[v0.2.2]: https://github.com/taoufik07/responder/compare/v0.2.1..v0.2.2
[v0.2.1]: https://github.com/taoufik07/responder/compare/v0.2.0..v0.2.1
[v0.2.0]: https://github.com/taoufik07/responder/compare/v0.1.6..v0.2.0
[v0.1.6]: https://github.com/taoufik07/responder/compare/v0.1.5..v0.1.6
[v0.1.5]: https://github.com/taoufik07/responder/compare/v0.1.4..v0.1.5
[v0.1.4]: https://github.com/taoufik07/responder/compare/v0.1.3..v0.1.4
[v0.1.3]: https://github.com/taoufik07/responder/compare/v0.1.2..v0.1.3
[v0.1.2]: https://github.com/taoufik07/responder/compare/v0.1.1..v0.1.2
[v0.1.1]: https://github.com/taoufik07/responder/compare/v0.1.0..v0.1.1
[v0.1.0]: https://github.com/taoufik07/responder/compare/v0.0.10..v0.1.0
[v0.0.10]: https://github.com/taoufik07/responder/compare/v0.0.9..v0.0.10
[v0.0.9]: https://github.com/taoufik07/responder/compare/v0.0.8..v0.0.9
[v0.0.8]: https://github.com/taoufik07/responder/compare/v0.0.7..v0.0.8
[v0.0.7]: https://github.com/taoufik07/responder/compare/v0.0.6..v0.0.7
[v0.0.6]: https://github.com/taoufik07/responder/compare/v0.0.5..v0.0.6
[v0.0.5]: https://github.com/taoufik07/responder/compare/v0.0.4..v0.0.5
[v0.0.4]: https://github.com/taoufik07/responder/compare/v0.0.3..v0.0.4
[v0.0.3]: https://github.com/taoufik07/responder/compare/v0.0.2..v0.0.3
[v0.0.2]: https://github.com/taoufik07/responder/compare/v0.0.1..v0.0.2
[v0.0.1]: https://github.com/taoufik07/responder/compare/v0.0.0..v0.0.1
+33
View File
@@ -0,0 +1,33 @@
# Development Sandbox
## Setup
Acquire sources and install project in editable mode.
```shell
git clone https://github.com/kennethreitz/responder
cd responder
python3 -m venv .venv
source .venv/bin/activate
pip install --editable '.[graphql,develop,release,test]'
```
## Operations
Invoke linter and software tests.
```shell
poe check
```
Format code.
```shell
poe format
```
## Release
```shell
git tag v2.1.0
git push --tags
poe release
```
+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
@@ -0,0 +1 @@
include LICENSE
-23
View File
@@ -1,23 +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 = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Generated
-644
View File
@@ -1,644 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "7bbe1f0addd73250027de73d6fb749aa2be3149af9744b107820c5e10498428e"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"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:7849749cf00ae0680ad2bdfe4419c7a662bef19c03691a19e008c8b9a5267802",
"sha256:94f90871fcd314a458a3d4eca1c84448efbd200e86f55fe4c733c7a40149ef50"
],
"version": "==3.0.2"
},
"apispec": {
"hashes": [
"sha256:c2e6ac6471aaf7c6ec6d12714821898910c6b3c87c189de9a2e3754786b86ada",
"sha256:fa7dfa8a292bae9b1e70c44a50bf61901805821726c5b804568c9f2501f57ebb"
],
"version": "==1.0.0b3"
},
"apistar": {
"hashes": [
"sha256:4338b24468b49526ceac4a8f84046056081ee747f373ca8d0647bd6b2344c895"
],
"version": "==0.6.0"
},
"asgiref": {
"hashes": [
"sha256:9b05dcd41a6a89ca8c6e7f7e4089c3f3e76b5af60aebb81ae6d455ad81989c97",
"sha256:b21dc4c43d7aba5a844f4c48b8f49d56277bc34937fd9f9cb93ec97fde7e3082"
],
"version": "==2.3.2"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"version": "==3.0.1"
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.10.15"
},
"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:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642",
"sha256:faa26573b598b22ffd274e2fd7a4c52efa405dcca96e01a62239482246248aa3"
],
"version": "==2.1.3"
},
"graphql-core": {
"hashes": [
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
],
"version": "==2.1"
},
"graphql-relay": {
"hashes": [
"sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db"
],
"version": "==0.4.5"
},
"graphql-server-core": {
"hashes": [
"sha256:e5f82add4b3d5580aa1f1e7d9f00e944ad3abe1b65eb337e611d6a77cc20f231"
],
"version": "==1.1.1"
},
"h11": {
"hashes": [
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
],
"version": "==0.8.1"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
],
"version": "==3.0.0b19"
},
"parse": {
"hashes": [
"sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437"
],
"version": "==1.9.0"
},
"promise": {
"hashes": [
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"version": "==2.2.1"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"version": "==0.0.5"
},
"pyyaml": {
"hashes": [
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
],
"version": "==4.2b4"
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"responder": {
"editable": true,
"path": "."
},
"rfc3986": {
"hashes": [
"sha256:632b8fcd2ac37f24334316227f909be4f9d0738cbf409404cff6fa5f69a24093",
"sha256:8458571c4c57e1cf23593ad860bb601b6a604df6217f829c2bc70dc4b5af941b"
],
"version": "==1.1.0"
},
"rx": {
"hashes": [
"sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23",
"sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"
],
"version": "==1.6.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"starlette": {
"hashes": [
"sha256:eac0f6cab6b48846a0c1af16615430ae0e7a95f669ee0841a7e2f242d51d8935"
],
"version": "==0.5.5"
},
"urllib3": {
"hashes": [
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.24"
},
"uvicorn": {
"hashes": [
"sha256:e2b742fdaa0b52f4aac92fd2c078e7f1f17d11322bb3efb09d341d5c6998b4b5"
],
"version": "==0.3.16"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
},
"whitenoise": {
"hashes": [
"sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02",
"sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039"
],
"version": "==4.1"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
],
"version": "==1.2.1"
},
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"babel": {
"hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"version": "==2.6.0"
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
],
"index": "pypi",
"version": "==18.9b0"
},
"bleach": {
"hashes": [
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
],
"version": "==3.0.2"
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.10.15"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
"sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.0"
},
"coverage": {
"hashes": [
"sha256:043d55226aec1d2baf4b2fcab5c204561ccf184a388096f41e396c1c092aff38",
"sha256:10bfd0b80b01d0684f968abbe1186bc19962e07b4b7601bb43b175b617cf689d",
"sha256:17e59864f19b3233032edb0566f26c25cc7f599503fb34d2645b5ce1fd6c2c3c",
"sha256:2105ee183c51fed27e2b6801029b3903f5c2774c78e3f53bd920ca468d0f5679",
"sha256:236505d15af6c7b7bfe2a9485db4b2bdea21d9239351483326184314418c79a8",
"sha256:237284425271db4f30d458b355decf388ab20b05278bdf8dc9a65de0973726c6",
"sha256:26d8eea4c840b73c61a1081d68bceb57b21a2d4f7afda6cac8ac38cb05226b00",
"sha256:39a3740f7721155f4269aedf67b211101c07bd2111b334dfd69b807156ab15d9",
"sha256:4bd0c42db8efc8a60965769796d43a5570906a870bc819f7388860aa72779d1b",
"sha256:4dcddadea47ac30b696956bd18365cd3a86724821656601151e263b86d34798f",
"sha256:51ea341289ac4456db946a25bd644f5635e5ae3793df262813cde875887d25c8",
"sha256:5415cafb082dad78935b3045c2e5d8907f436d15ad24c3fdb8e1839e084e4961",
"sha256:5631f1983074b33c35dbb84607f337b9d7e9808116d7f0f2cb7b9d6d4381d50e",
"sha256:5e9249bc361cd22565fd98590a53fd25a3dd666b74791ed7237fa99de938bbed",
"sha256:6a48746154f1331f28ef9e889c625b5b15a36cb86dd8021b4bdd1180a2186aa5",
"sha256:71d376dbac64855ed693bc1ca121794570fe603e8783cdfa304ec6825d4e768f",
"sha256:749ebd8a615337747592bd1523dfc4af7199b2bf6403b55f96c728668aeff91f",
"sha256:8ec528b585b95234e9c0c31dcd0a89152d8ed82b4567aa62dbcb3e9a0600deee",
"sha256:a1a9ccd879811437ca0307c914f136d6edb85bd0470e6d4966c6397927bcabd9",
"sha256:abd956c334752776230b779537d911a5a12fcb69d8fd3fe332ae63a140301ae6",
"sha256:ad18f836017f2e8881145795f483636564807aaed54223459915a0d4735300cf",
"sha256:b07ac0b1533298ddbc54c9bf3464664895f22899fec027b8d6c8d3ac59023283",
"sha256:d9385f1445e30e8e42b75a36a7899ea1fd0f5784233a626625d70f9b087de404",
"sha256:db2d1fcd32dbeeb914b2660af1838e9c178b75173f95fd221b1f9410b5d3ef1d",
"sha256:e1dec211147f1fd7cb7a0f9a96aeeca467a5af02d38911307b3b8c2324f9917e",
"sha256:e96dffc1fa57bb8c1c238f3d989341a97302492d09cb11f77df031112621c35c",
"sha256:ed4d97eb0ecdee29d0748acd84e6380729f78ce5ba0c7fe3401801634c25a1c5"
],
"version": "==5.0a3"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
],
"version": "==0.14"
},
"flake8": {
"hashes": [
"sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
"sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
],
"index": "pypi",
"version": "==3.6.0"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"future": {
"hashes": [
"sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4"
],
"version": "==0.17.0"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"marshmallow": {
"hashes": [
"sha256:5e0053c86e3abaa72a03bbe0021ec97270c13fd6400b682eb1aeaf24b871bc8a",
"sha256:81884e930c1db72d8b8e3d8d2d090f2f43427e5c11c37f703b29879980491ab6"
],
"version": "==3.0.0b19"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
],
"version": "==4.3.0"
},
"packaging": {
"hashes": [
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
],
"version": "==18.0"
},
"pkginfo": {
"hashes": [
"sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474",
"sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee"
],
"version": "==1.4.2"
},
"pluggy": {
"hashes": [
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
],
"version": "==0.8.0"
},
"py": {
"hashes": [
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
],
"version": "==1.7.0"
},
"pycodestyle": {
"hashes": [
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
],
"version": "==2.4.0"
},
"pyflakes": {
"hashes": [
"sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
"sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
],
"version": "==2.0.0"
},
"pygments": {
"hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
],
"version": "==2.2.0"
},
"pyparsing": {
"hashes": [
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
],
"version": "==2.2.2"
},
"pytest": {
"hashes": [
"sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
"sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
],
"index": "pypi",
"version": "==3.9.2"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
],
"index": "pypi",
"version": "==2.6.0"
},
"pytz": {
"hashes": [
"sha256:642253af8eae734d1509fc6ac9c1aee5e5b69d76392660889979b9870610a46b",
"sha256:91e3ccf2c344ffaa6defba1ce7f38f97026943f675b7703f44789768e4cb0ece"
],
"version": "==2018.6"
},
"readme-renderer": {
"hashes": [
"sha256:219a02f5359b6631f5ab952f6906c6c105bdd8bc4bf19c1ec5ee8bd9ea2dc1eb",
"sha256:f8f122ad9fd6d138337531379575a01a0b6ca70aedca78f094cb833da38c8c0c"
],
"version": "==23.0"
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"snowballstemmer": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
],
"version": "==1.2.1"
},
"sphinx": {
"hashes": [
"sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17",
"sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464"
],
"index": "pypi",
"version": "==1.8.1"
},
"sphinxcontrib-websupport": {
"hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"tqdm": {
"hashes": [
"sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
"sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
],
"version": "==4.28.1"
},
"twine": {
"hashes": [
"sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c",
"sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c"
],
"index": "pypi",
"version": "==1.12.1"
},
"urllib3": {
"hashes": [
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
],
"version": "==1.24"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
}
}
}
+50 -33
View File
@@ -1,74 +1,91 @@
# Responder: a familiar HTTP Service Framework for Python
[![Build Status](https://travis-ci.org/kennethreitz/responder.svg?branch=master)](https://travis-ci.org/kennethreitz/responder)
[![Documentation Status](https://readthedocs.org/projects/mybinder/badge/?version=latest)](https://responder.readthedocs.io/en/latest/)
[![Build Status](https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg)](https://github.com/kennethreitz/responder/actions/workflows/test.yaml)
[![Documentation Status](https://github.com/kennethreitz/responder/actions/workflows/pages/pages-build-deployment/badge.svg)](https://responder.kennethreitz.org/)
[![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/kennethreitz/responder.svg)](https://github.com/kennethreitz/responder/graphs/contributors)
[![PyPI Downloads](https://pepy.tech/badge/responder/month)](https://pepy.tech/project/responder/)
[![Status](https://img.shields.io/pypi/status/responder.svg)](https://pypi.org/project/responder/)
[![License](https://img.shields.io/pypi/l/responder.svg)](https://pypi.org/project/responder/)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](http://python-responder.org/)
[![](https://farm2.staticflickr.com/1959/43750081370_a4e20752de_o_d.png)](https://responder.readthedocs.io)
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional.
[View documentation](https://responder.readthedocs.io).
Powered by [Starlette](https://www.starlette.io/). That `async` declaration is optional. [View documentation](http://python-responder.org).
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.
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.
## 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://twitter.com/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](https://www.django-rest-framework.org/)
> "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]()
> "Love what I have seen while it's in progress! Many features of Responder are from my wishlist for Flask, and it's even faster and even easier than Flask!" — Luna C.
> "I love that you are exploring new patterns. Go go go!" — Danny Greenfield, author of
> [Two Scoops of Django]()
## More Examples
See [the documentation's feature tour](http://python-responder.org/en/latest/tour.html) for more details on features available in Responder.
See
[the documentation's feature tour](https://responder.readthedocs.io/en/latest/tour.html)
for more details on features available in Responder.
# Installing Responder
Install the latest release:
Install the most recent stable release:
pip install --upgrade responder
$ pipenv install responder --pre
✨🍰✨
Or, install directly from the repository:
Or, install from the development branch:
$ pipenv install -e git+https://github.com/kennethreitz/responder.git#egg=responder
pip install 'responder @ git+https://github.com/kennethreitz/responder.git'
Only **Python 3.6+** is supported.
# The Basic Idea
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests.
The primary concept here is to bring the niceties that are brought forth from both Flask
and Falcon and unify them into a single framework, along with some new ideas I have. I
also wanted to take some of the API primitives that are instilled in the Requests
library and put them into a web framework. So, you'll find a lot of parallels here with
Requests.
- Setting `resp.text` sends back unicode, while setting `resp.content` sends back bytes.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.content` override this).
- Setting `resp.content` sends back bytes.
- Setting `resp.text` sends back unicode, while setting `resp.html` sends back HTML.
- Setting `resp.media` sends back JSON/YAML (`.text`/`.html`/`.content` override this).
- Case-insensitive `req.headers` dict (from Requests directly).
- `resp.status_code`, `req.method`, `req.url`, and other familiar friends.
## Ideas
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- 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.
- 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.
- 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
----------
[![hacktoberfest](https://hacktoberfest.digitalocean.com/assets/hacktoberfest-2018-social-card-c8d2e1489f647f2e0a26e6f598adeb760872818905b34cd437afc7ac2857ceab.png)](https://hacktoberfest.digitalocean.com/)
See [Development Sandbox](DEVELOP.md).
+37
View File
@@ -0,0 +1,37 @@
# ruff: noqa: S605, S607
"""
Build and publish a .deb package.
https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb
"""
import os
from shutil import rmtree
here = os.path.abspath(os.path.dirname(__file__))
def get_version():
import responder
return responder.__version__
def run():
version = get_version()
try:
print("Removing previous builds")
rmtree(os.path.join(here, "deb_dist"))
except FileNotFoundError:
pass
print("Creating Debian package manifest")
os.system(
"python setup.py --command-packages=stdeb.command sdist_dsc "
"-z artful --package3=pipenv --depends3=python3-virtualenv-clone"
)
print("Building .deb")
os.chdir(f"deb_dist/pipenv-{version}")
os.system("dpkg-buildpackage -rfakeroot -uc -us")
if __name__ == "__main__":
run()
+6 -48
View File
@@ -1,48 +1,6 @@
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.14.1
alabaster<0.8
jinja2<3.2
markupsafe<4
readme-renderer<45
sphinx>=5,<9
sphinxcontrib-websupport<2.1
+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;
}
}
+72 -38
View File
@@ -1,5 +1,11 @@
<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 type="text/javascript">
$("#searchbox").hide(0);
</script>
<!--Alabaster (krTheme++) Hacks -->
<!-- CSS Adjustments (I'm very picky.) -->
@@ -26,7 +32,7 @@
.class em,
.descname,
.method em {
font-family: "Operator Mono SSm A", "Operator Mono SSm B" !important;
font-family: "Operator Mono SSm A", "Operator Mono SSm B", monospace !important;
font-weight: 400 !important;
}
@@ -39,9 +45,7 @@
}
.method {
margin-bottom: 2em;
}
.si,
@@ -80,8 +84,6 @@
margin-top: -1em;
}
/* "Quick Search" should be not be shown for now. */
div#searchbox h3 {
display: none;
@@ -118,10 +120,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 +134,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 +147,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>
+73 -13
View File
@@ -1,33 +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"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<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://twitter.com/kennethreitz"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
+73 -13
View File
@@ -1,33 +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"
/>
<style>
.algolia-autocomplete {
width: 100%;
height: 1.5em;
}
.algolia-autocomplete a {
border-bottom: none !important;
}
#doc_search {
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus />
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
onload="docsearch({
apiKey: 'ac965312db252e0496283c75c6f76f0b',
indexName: 'python-responder',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})"
async
></script>
<p>
<strong>Responder</strong> is a web service framework, written for human beings.
</p>
<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://twitter.com/kennethreitz"
class="twitter-follow-button"
data-show-count="false"
>Follow @kennethreitz</a
>
<script>
!(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
p = /^http:/.test(d.location) ? "http" : "https";
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
})(document, "script", "twitter-wjs");
</script>
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/responder">Responder @ PyPI</a></li>
<li><a href="http://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
+3 -15
View File
@@ -20,23 +20,11 @@
# -- Project information -----------------------------------------------------
project = "responder"
copyright = "2018, A Kenneth Reitz project"
copyright = "2024, A Kenneth Reitz project"
author = "Kenneth Reitz"
# The short X.Y version
import os
# Path hackery to get current version number.
here = os.path.abspath(os.path.dirname(__file__))
about = {}
with open(os.path.join(here, "..", "..", "responder", "__version__.py")) as f:
exec(f.read(), about)
version = about["__version__"]
# The full version, including alpha/beta/rc tags
release = about["__version__"]
version = ""
# -- General configuration ---------------------------------------------------
@@ -76,7 +64,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.
+3 -2
View File
@@ -11,9 +11,10 @@ Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``::
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
EXPOSE 80
That's it!
@@ -30,7 +31,7 @@ The basics::
Install Responder::
$ pipenv install responder --pre
$ pipenv install responder
...
Write out an ``api.py``::
+17 -24
View File
@@ -8,8 +8,8 @@ A familiar HTTP Service Framework
|Build Status| |image1| |image2| |image3| |image4| |image5|
.. |Build Status| image:: https://travis-ci.org/kennethreitz/responder.svg?branch=master
:target: https://travis-ci.org/kennethreitz/responder
.. |Build Status| image:: https://github.com/kennethreitz/responder/actions/workflows/test.yaml/badge.svg
:target: https://github.com/kennethreitz/responder/actions/workflows/test.yaml
.. |image1| image:: https://img.shields.io/pypi/v/responder.svg
:target: https://pypi.org/project/responder/
.. |image2| image:: https://img.shields.io/pypi/l/responder.svg
@@ -36,19 +36,21 @@ A familiar HTTP Service Framework
Powered by `Starlette <https://www.starlette.io/>`_. That ``async`` declaration is optional.
This gets you a ASGI app, with a production static files server (WhiteNoise)
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 gzip
compression automatically.
production webserver based on uvloop, serving up requests with
automatic gzip compression.
Features
--------
- A pleasant API, with a single import statement.
- Class-based views without inheritence.
- ASGI framework, the future of Python web services.
- Class-based views without inheritance.
- `ASGI <https://asgi.readthedocs.io>`_ framework, the future of Python web services.
- WebSocket support!
- The ability to mount any ASGI / WSGI app at a subroute.
- *f-string syntax* route declaration.
- `f-string syntax <https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals>`_ route declaration.
- Mutable response object, passed into each view. No need to return anything.
- Background tasks, spawned off in a ``ThreadPoolExecutor``.
- GraphQL (with *GraphiQL*) support!
@@ -80,18 +82,8 @@ Testimonials
— Danny Greenfield, author of `Two Scoops of Django`_
..
“The most ambitious crossover event in history.”
—Pablo Cabezas, `on Tom Christie joining the project`_
.. _APIStar: https://github.com/encode/apistar
.. _Django REST Framework: https://www.django-rest-framework.org/
.. _Two Scoops of Django:
.. _on Tom Christie joining the project: https://twitter.com/pabloteleco/status/1050841098321620992?s=20
.. _Two Scoops of Django: https://www.feldroy.com/two-scoops-press#two-scoops-of-django
User Guides
-----------
@@ -111,7 +103,7 @@ Installing Responder
.. code-block:: shell
$ pipenv install responder --pre
$ pipenv install responder
✨🍰✨
Only **Python 3.6+** is supported.
@@ -122,8 +114,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.
- Setting ``resp.text`` sends back unicode, while setting ``resp.content`` sends back bytes.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.content`` override this).
- Setting ``resp.content`` sends back bytes.
- Setting ``resp.text`` sends back unicode, while setting ``resp.html`` sends back HTML.
- Setting ``resp.media`` sends back JSON/YAML (``.text``/``.html``/``.content`` override this).
- Case-insensitive ``req.headers`` dict (from Requests directly).
- ``resp.status_code``, ``req.method``, ``req.url``, and other familiar friends.
@@ -131,13 +124,13 @@ Ideas
-----
- Flask-style route expression, with new capabilities -- all while using Python 3.6+'s new f-string syntax.
- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- I love Falcon's "every request and response is passed into each view and mutated" methodology, especially ``response.media``, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that.
- **A built in testing client that uses the actual Requests you know and love**.
- The ability to mount other WSGI apps easily.
- Automatic gzipped-responses.
- In addition to Falcon's ``on_get``, ``on_post``, etc methods, Responder features an ``on_request`` method, which gets called on every type of request, much like Requests.
- A production static files server is built-in.
- Uvicorn 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 unneccessary in production.
- `Uvicorn <https://www.uvicorn.org/>`_ is built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Uvicorn serves well to protect against `slowloris <https://en.wikipedia.org/wiki/Slowloris_(computer_security)>`_ attacks, making nginx unnecessary in production.
- GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
+54 -3
View File
@@ -48,6 +48,14 @@ If you want dynamic URLs, you can use Python's familiar *f-string syntax* to dec
A ``GET`` request to ``/hello/brettcannon`` will result in a response of ``hello, brettcannon!``.
Type convertors are also available::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Supported types: ``str``, ``int`` and ``float``.
Returning JSON / YAML
---------------------
@@ -65,13 +73,30 @@ If the client requests YAML instead (with a header of ``Accept: application/x-ya
Rendering a Template
--------------------
If you want to render a template, simply use ``api.template``. No need for additional imports::
Responder provides a built-in light `jinja2 <http://jinja.pocoo.org/docs/>`_ wrapper ``templates.Templates``
Usage::
from responder.templates import Templates
templates = Templates()
@api.route("/hello/{name}/html")
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::
@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
resp.content = api.template('hello.html', who=who)
resp.html = api.template('hello.html', who=who)
The ``api`` instance is available as an object during template rendering.
Setting Response Status Code
----------------------------
@@ -124,3 +149,29 @@ Here, we'll process our data in the background, while responding immediately to
resp.media = {'success': True}
A ``POST`` request to ``/incoming`` will result in an immediate response of ``{'success': true}``.
Here's a sample code to post a file with background::
@api.route("/")
async def upload_file(req, resp):
@api.background.task
def process_data(data):
f = open('./{}'.format(data['file']['filename']), 'w')
f.write(data['file']['content'].decode('utf-8'))
f.close()
data = await req.media(format='files')
process_data(data)
resp.media = {'success': 'ok'}
You can send a file easily with requests::
import requests
data = {'file': ('hello.txt', 'hello, world!', "text/plain")}
r = requests.post('http://127.0.0.1:8210/file', files=data)
print(r.text)
+201 -16
View File
@@ -52,19 +52,44 @@ Serve a GraphQL API::
Visiting the endpoint will render a *GraphiQL* instance, in the browser.
You can make use of Responder's Request and Response objects in your GraphQL resolvers through ``info.context['request']`` and ``info.context['response']``.
OpenAPI 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`::
import responder
from responder.ext.schema import Schema as OpenAPISchema
from marshmallow import Schema, fields
api = responder.API(title="Web Service", version="1.0", openapi="3.0")
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API()
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@api.schema("Pet")
@schema.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@@ -78,46 +103,134 @@ Responder comes with built-in support for OpenAPI / marshmallow::
responses:
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
Old way *It's recommended to use the code above* ::
import responder
from marshmallow import Schema, fields
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
description="A simple pet store",
terms_of_service="http://example.com/terms/",
contact=contact,
license=license,
)
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
::
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
schemas:
parameters: {}
responses: {}
schemas:
Pet:
properties:
properties:
name: {type: string}
type: object
info: {title: Web Service, version: 1.0}
openapi: '3.0'
type: object
securitySchemes: {}
info:
contact: {email: support@example.com, name: API Support, url: 'http://www.example.com/support'}
description: This is a sample server for a pet store.
license: {name: Apache 2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0.html'}
termsOfService: http://example.com/terms/
title: Web Service
version: 1.0
openapi: 3.0.2
paths:
/:
get:
description: Get a random pet
responses:
200: {description: A pet to be returned, schema: $ref = "#/components/schemas/Pet"}
200: {description: A pet to be returned, schema: $ref: "#/components/schemas/Pet"}
tags: []
Interactive Documentation
-------------------------
Responder can automatically supply API Documentation for you. Using the example above::
Responder can automatically supply API Documentation for you. Using the example above
api = responder.API(title="Web Service", version="1.0", openapi="3.0", docs_route="/docs")
The new and recommended way::
...
from responder.ext.schema import Schema
...
api = responder.API()
schema = Schema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
...
docs_route='/docs',
...
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
The old way ::
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route='/docs',
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
This will make ``/docs`` render interactive documentation for your API.
Mount a WSGI App (e.g. Flask)
-----------------------------
Mount a WSGI / ASGI Apps (e.g. Flask, Starlette,...)
----------------------------------------------------
Responder gives you the ability to mount another ASGI / WSGI app at a subroute::
@@ -155,6 +268,24 @@ Responder makes it very easy to interact with cookies from a Request, or add som
{"hello": "world"}
To set cookies directives, you should use `resp.set_cookie`::
>>> resp.set_cookie("hello", value="world", max_age=60)
Supported directives:
* ``key`` - **Required**
* ``value`` - [OPTIONAL] - Defaults to ``""``.
* ``expires`` - Defaults to ``None``.
* ``max_age`` - Defaults to ``None``.
* ``domain`` - Defaults to ``None``.
* ``path`` - Defaults to ``"/"``.
* ``secure`` - Defaults to ``False``.
* ``httponly`` - Defaults to ``True``.
For more information see `directives <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>`_
Using Cookie-Based Sessions
---------------------------
@@ -184,6 +315,41 @@ If you'd like a view to be executed before every request, simply do the followin
Now all requests to your HTTP Service will include an ``X-Pizza`` header.
For ``websockets``::
@api.route(before_request=True, websocket=True)
def prepare_response(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets::
@api.route('/ws', websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
Accepting the connection::
await websocket.accept()
Sending and receiving data::
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
Supported formats: ``text``, ``json``, ``bytes``.
Closing the connection::
await websocket.close()
Using Requests Test Client
--------------------------
@@ -240,3 +406,22 @@ In order to set custom parameters, you need to set the ``cors_params`` argument
* ``allow_credentials`` - Indicate that cookies should be supported for cross-origin requests. Defaults to ``False``.
* ``expose_headers`` - Indicate any response headers that should be made accessible to the browser. Defaults to ``[]``.
* ``max_age`` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to ``60``.
Trusted Hosts
-------------
Make sure that all the incoming requests headers have a valid ``host``, that matches one of the provided patterns in the ``allowed_hosts`` attribute, in order to prevent HTTP Host Header attacks.
A 400 response will be raised, if a request does not match any of the provided patterns in the ``allowed_hosts`` attribute.
::
api = responder.API(allowed_hosts=['example.com', 'tenant.example.com'])
* ``allowed_hosts`` - A list of allowed hostnames.
Note:
* By default, all hostnames are allowed.
* Wildcard domains such as ``*.example.com`` are supported.
* To allow any hostname use ``allowed_hosts=["*"]``.
+12
View File
@@ -0,0 +1,12 @@
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
+121
View File
@@ -0,0 +1,121 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=42", # At least v42 of setuptools required.
"versioningit",
]
[tool.ruff]
line-length = 90
extend-exclude = [
"bin/mkdeb.py",
"docs/source/conf.py",
"setup.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."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.versioningit]
[tool.poe.tasks]
check = [
"lint",
"test",
]
docs-autobuild = [
{ cmd = "sphinx-autobuild --open-browser --watch 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
+2 -2
View File
@@ -1,5 +1,5 @@
build:
image: latest
image: latest
python:
version: 3.6
version: 3.6
+16 -1
View File
@@ -1,2 +1,17 @@
from .core import *
from importlib.metadata import PackageNotFoundError, version
from . import ext
from .core import API, Request, Response
try:
__version__ = version("responder")
except PackageNotFoundError: # pragma: no cover
__version__ = "unknown"
__all__ = [
"API",
"Request",
"Response",
"ext",
"__version__",
]
-4
View File
@@ -1,4 +0,0 @@
from .cli import main
if __name__ == "__main__":
main()
-1
View File
@@ -1 +0,0 @@
__version__ = "1.1.0"
+153 -401
View File
@@ -1,52 +1,35 @@
import json
import os
from uuid import uuid4
from pathlib import Path
from base64 import b64encode
import apistar
import itsdangerous
import jinja2
import uvicorn
import yaml
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from asgiref.wsgi import WsgiToAsgi
from starlette.debug import DebugMiddleware
from starlette.exceptions import ExceptionMiddleware
from starlette.lifespan import LifespanHandler
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.testclient import TestClient
from starlette.websockets import WebSocket
from whitenoise import WhiteNoise
from . import models, status_codes
from . import status_codes
from .background import BackgroundQueue
from .ext.schema import OpenAPISchema as OpenAPISchema
from .formats import get_formats
from .routes import Route
from .statics import (
DEFAULT_API_THEME,
DEFAULT_CORS_PARAMS,
DEFAULT_SECRET_KEY,
DEFAULT_SESSION_COOKIE,
)
from .templates import GRAPHIQL
from .routes import Router
from .staticfiles import StaticFiles
from .statics import DEFAULT_CORS_PARAMS, DEFAULT_OPENAPI_THEME, DEFAULT_SECRET_KEY
from .templates import Templates
# TODO: consider moving status codes here
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
@@ -56,6 +39,10 @@ class API:
debug=False,
title=None,
version=None,
description=None,
terms_of_service=None,
contact=None,
license=None, # noqa: A002
openapi=None,
openapi_route="/schema.yml",
static_dir="static",
@@ -66,178 +53,112 @@ class API:
enable_hsts=False,
docs_route=None,
cors=False,
cors_params=DEFAULT_CORS_PARAMS,
allowed_hosts=None,
openapi_theme=DEFAULT_OPENAPI_THEME,
):
self.background = BackgroundQueue()
self.secret_key = secret_key
self.title = title
self.version = version
self.openapi_version = openapi
self.static_dir = Path(os.path.abspath(static_dir))
self.router = Router()
if static_dir is not None:
if static_route is None:
static_route = static_dir
static_dir = Path(os.path.abspath(static_dir))
self.static_dir = static_dir
self.static_route = static_route
self.templates_dir = Path(os.path.abspath(templates_dir))
self.built_in_templates_dir = Path(
os.path.abspath(os.path.dirname(__file__) + "/templates")
)
self.routes = {}
self.docs_theme = DEFAULT_API_THEME
self.docs_route = docs_route
self.schemas = {}
self.session_cookie = DEFAULT_SESSION_COOKIE
self.hsts_enabled = enable_hsts
self.cors = cors
self.cors_params = DEFAULT_CORS_PARAMS
# Make the static/templates directory if they don't exist.
for _dir in (self.static_dir, self.templates_dir):
os.makedirs(_dir, exist_ok=True)
self.cors_params = cors_params
self.debug = debug
self.whitenoise = WhiteNoise(application=self._default_wsgi_app)
self.whitenoise.add_files(str(self.static_dir))
if not allowed_hosts:
# if not debug:
# raise RuntimeError(
# "You need to specify `allowed_hosts` when debug is set to False"
# ) # noqa: ERA001
allowed_hosts = ["*"]
self.allowed_hosts = allowed_hosts
self.whitenoise.add_files(
(
Path(apistar.__file__).parent / "themes" / self.docs_theme / "static"
).resolve()
)
if self.static_dir is not None:
os.makedirs(self.static_dir, exist_ok=True)
self.apps = {}
self.mount(self.static_route, self.whitenoise)
if self.static_dir is not None:
self.mount(self.static_route, self.static_app)
self.formats = get_formats()
# Cached requests session.
self._session = None
self.background = BackgroundQueue()
if self.openapi_version:
self.add_route(openapi_route, self.schema_response)
if self.docs_route:
self.add_route(self.docs_route, self.docs_response)
self.default_endpoint = None
self.app = self.dispatch
self.app = ExceptionMiddleware(self.router, debug=debug)
self.add_middleware(GZipMiddleware)
if debug:
self.add_middleware(DebugMiddleware)
if self.hsts_enabled:
self.add_middleware(HTTPSRedirectMiddleware)
self.lifespan_handler = LifespanHandler()
self.add_middleware(TrustedHostMiddleware, allowed_hosts=self.allowed_hosts)
if self.cors:
self.add_middleware(CORSMiddleware, **self.cors_params)
self.add_middleware(ExceptionMiddleware, debug=debug)
self.add_middleware(ServerErrorMiddleware, debug=debug)
self.add_middleware(SessionMiddleware, secret_key=self.secret_key)
# Jinja enviroment
self.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
[str(self.templates_dir), str(self.built_in_templates_dir)],
followlinks=True,
),
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
self.jinja_values_base = {"api": self} # Give reference to self.
if openapi or docs_route:
self.openapi = OpenAPISchema(
app=self,
title=title,
version=version,
openapi=openapi,
docs_route=docs_route,
description=description,
terms_of_service=terms_of_service,
contact=contact,
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.
@staticmethod
def _default_wsgi_app(*args, **kwargs):
pass
@property
def before_requests(self):
def gen():
for route in self.routes:
if self.routes[route].before_request:
yield self.routes[route]
def static_app(self):
if not hasattr(self, "_static_app"):
assert self.static_dir is not None
self._static_app = StaticFiles(directory=self.static_dir)
return self._static_app
return [g for g in gen()]
def before_request(self, websocket=False):
def decorator(f):
self.router.before_request(f, websocket=websocket)
return f
@property
def _apispec(self):
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=[MarshmallowPlugin()],
)
for route in self.routes:
if self.routes[route].description:
operations = yaml_utils.load_operations_from_docstring(
self.routes[route].description
)
spec.add_path(path=route, operations=operations)
for name, schema in self.schemas.items():
spec.definition(name, schema=schema)
return spec
@property
def openapi(self):
return self._apispec.to_yaml()
return decorator
def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config)
def __call__(self, scope):
if scope["type"] == "lifespan":
return self.lifespan_handler(scope)
path = scope["path"]
root_path = scope.get("root_path", "")
# Call into a submounted app, if one exists.
for path_prefix, app in self.apps.items():
if path.startswith(path_prefix):
scope["path"] = path[len(path_prefix) :]
scope["root_path"] = root_path + path_prefix
try:
return app(scope)
except TypeError:
app = WsgiToAsgi(app)
return app(scope)
return self.app(scope)
def dispatch(self, scope):
# Call the main dispatcher.
async def asgi(receive, send):
nonlocal scope, self
req = models.Request(scope, receive=receive, api=self)
resp = await self._dispatch_request(
req, scope=scope, send=send, receive=receive
)
await resp(receive, send)
return asgi
def add_schema(self, name, schema, check_existing=True):
"""Adds a mashmallow schema to the API specification."""
if check_existing:
assert name not in self.schemas
self.schemas[name] = schema
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):
name = fields.Str()
"""
def decorator(f):
self.add_schema(name=name, schema=f, **options)
self.openapi.add_schema(name=name, schema=f, **options)
return f
return decorator
@@ -246,118 +167,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.
"""
for (route, route_object) in self.routes.items():
if route_object.does_match(path):
""" # noqa: E501 (Line too long)
for route in self.router.routes:
match, _ = route.matches(path)
if match:
return route
def _prepare_cookies(self, resp):
if resp.cookies:
header = " ".join([f"{k}={v};" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
@property
def _signer(self):
return itsdangerous.Signer(self.secret_key)
def _prepare_session(self, resp):
if resp.session:
data = self._signer.sign(
b64encode(json.dumps(resp.session).encode("utf-8"))
)
resp.cookies[self.session_cookie] = data.decode("utf-8")
@staticmethod
def no_response(req, resp, **params):
pass
async def _dispatch_request(self, req, **options):
# Set formats on Request object.
req.formats = self.formats
# Get the route.
route = self.path_matches_route(req.url.path)
route = self.routes.get(route)
if route:
if route.uses_websocket:
resp = WebSocket(**options)
else:
resp = models.Response(req=req, formats=self.formats)
for before_request in self.before_requests:
await self._execute_route(route=before_request, req=req, resp=resp)
await self._execute_route(route=route, req=req, resp=resp, **options)
else:
resp = models.Response(req=req, formats=self.formats)
self.default_response(req, resp, notfound=True)
self.default_response(req, resp)
self._prepare_session(resp)
self._prepare_cookies(resp)
return resp
async def _execute_route(self, *, route, req, resp, **options):
params = route.incoming_matches(req.url.path)
if route.is_function:
try:
try:
# Run the view.
r = route.endpoint(req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
except Exception:
self.default_response(req, resp, error=True)
raise
elif route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
try:
view = route.endpoint()
except TypeError:
view = route.endpoint
# Run on_request first.
try:
# Run the view.
r = getattr(view, "on_request", self.no_response)(req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception:
self.default_response(req, resp, error=True)
raise
# Then run on_method.
method = req.method
try:
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)(req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
except Exception as e:
self.default_response(req, resp, error=True)
def add_event_handler(self, event_type, handler):
"""Adds an event handler to the API.
:param event_type: A string in ("startup", "shutdown")
:param handler: The function to run. Can be either a function or a coroutine.
"""
self.lifespan_handler.add_event_handler(event_type, handler)
return None
def add_route(
self,
@@ -365,7 +180,7 @@ class API:
endpoint=None,
*,
default=False,
static=False,
static=True,
check_existing=True,
websocket=False,
before_request=False,
@@ -375,80 +190,52 @@ class 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 check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
"""
if route is None:
route = f"/{uuid4().hex}"
:param static: If ``True``, and no endpoint was passed, render "static/index.html".
Also, it will become a default route.
""" # noqa: E501
if check_existing:
assert route not in self.routes
# Path
if static:
assert self.static_dir is not None
if not endpoint:
endpoint = self._static_response
default = True
if not endpoint and static:
endpoint = self.static_response
default = True
if default:
self.default_endpoint = endpoint
self.routes[route] = Route(
route, endpoint, websocket=websocket, before_request=before_request
)
# TODO: A better data structure or sort it once the app is loaded
self.routes = dict(
sorted(self.routes.items(), key=lambda item: item[1]._weight())
self.router.add_route(
route,
endpoint,
default=default,
websocket=websocket,
before_request=before_request,
check_existing=check_existing,
)
def default_response(self, req, resp, notfound=False, error=False):
if resp.status_code is None:
resp.status_code = 200
async def _static_response(self, req, resp):
assert self.static_dir is not None
if self.default_endpoint and notfound:
self.default_endpoint(req, resp)
else:
if notfound:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
if error:
resp.status_code = status_codes.HTTP_500
resp.text = "Application error."
def docs_response(self, req, resp):
resp.text = self.docs
def static_response(self, req, resp):
index = (self.static_dir / "index.html").resolve()
resp.content = ""
if os.path.exists(index):
with open(index, "r") as f:
resp.text = f.read()
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
resp.headers["Content-Type"] = "application/x-yaml"
resp.content = self.openapi
resp.html = f.read()
else:
resp.status_code = status_codes.HTTP_404
resp.text = "Not found."
def redirect(
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
):
"""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.
"""
# assert resp.status_code.is_300(status_code)
resp.status_code = status_code
if set_text:
resp.text = f"Redirecting to: {location}"
resp.headers.update({"Location": location})
resp.redirect(location, set_text=set_text, status_code=status_code)
def on_event(self, event_type: str, **args):
"""Decorator for registering functions or coroutines to run at certain events
Supported events: startup, cleanup, shutdown, tick
Supported events: startup, shutdown
Usage::
@@ -456,11 +243,7 @@ class API:
async def open_database_connection_pool():
...
@api.on_event('tick', seconds=10)
async def do_stuff():
...
@api.on_event('cleanup')
@api.on_event('shutdown')
async def close_database_connection_pool():
...
@@ -472,6 +255,15 @@ class API:
return decorator
def add_event_handler(self, event_type, handler):
"""Adds an event handler to the API.
:param event_type: A string in ("startup", "shutdown")
:param handler: The function to run. Can be either a function or a coroutine.
"""
self.router.add_event_handler(event_type, handler)
def route(self, route=None, **options):
"""Decorator for creating new routes around function and class definitions.
@@ -492,114 +284,66 @@ class API:
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.apps.update({route: 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 Requests session object,
able to send HTTP requests to the Responder application.
:param base_url: The URL to mount the connection adaptor to.
"""
if self._session is None:
self._session = TestClient(self)
self._session = TestClient(self, base_url=base_url)
return self._session
def _route_for(self, endpoint):
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object
elif route_object.endpoint_name == endpoint:
return route_object
def url_for(self, endpoint, **params):
# TODO: Absolute_url
"""Given an endpoint, returns a rendered URL for its route.
:param view: The route endpoint you're searching for.
:param endpoint: The route endpoint you're searching for.
:param params: Data to pass into the URL generator (for parameterized URLs).
"""
route_object = self._route_for(endpoint)
if route_object:
return route_object.url(**params)
raise ValueError
return self.router.url_for(endpoint, **params)
def static_url(self, asset):
"""Given a static asset, return its URL path."""
return f"{self.static_route}/{str(asset)}"
@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"]))
def static_url(asset):
return f"{self.static_route}/{asset}"
# return asset
return template.render(
document=document,
langs=["javascript", "python"],
code_style=None,
static_url=static_url,
schema_url="/schema.yml",
)
def template(self, name_, **values):
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``.
:param name_: The filename of the jinja2 template, in ``templates_dir``.
:param values: Data to pass into the template.
"""
# Prepopulate values with base
values = {**self.jinja_values_base, **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.
""" # noqa: E501
return self.templates.render(filename, *args, **kwargs)
template = self.jinja_env.get_template(name_)
return template.render(**values)
def template_string(self, s_, **values):
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.
""" # noqa: E501
return self.templates.render_string(source, *args, **kwargs)
:param s_: The template to use.
:param values: Data to pass into the template.
"""
# Prepopulate values with base
values = {**self.jinja_values_base, **values}
template = self.jinja_env.from_string(s_)
return template.render(**values)
def run(self, address=None, port=None, debug=False, **options):
def serve(self, *, address=None, port=None, debug=False, **options):
"""Runs the application with uvicorn. If the ``PORT`` environment
variable is set, requests will be served on that port automatically to all
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:
@@ -611,3 +355,11 @@ class API:
uvicorn.run(self, host=address, port=port, debug=debug, **options)
spawn()
def run(self, **kwargs):
if "debug" not in kwargs:
kwargs.update({"debug": self.debug})
self.serve(**kwargs)
async def __call__(self, scope, receive, send):
await self.app(scope, receive, send)
+11 -3
View File
@@ -1,6 +1,9 @@
import traceback
import multiprocessing
import asyncio
import concurrent.futures
import multiprocessing
import traceback
from starlette.concurrency import run_in_threadpool
class BackgroundQueue:
@@ -24,7 +27,7 @@ class BackgroundQueue:
def on_future_done(fs):
try:
fs.result()
except:
except Exception:
traceback.print_exc()
def do_task(*args, **kwargs):
@@ -33,3 +36,8 @@ class BackgroundQueue:
return result
return do_task
async def __call__(self, func, *args, **kwargs) -> None:
if asyncio.iscoroutinefunction(func):
return await asyncio.ensure_future(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
-64
View File
@@ -1,64 +0,0 @@
import json
from functools import partial
from graphql_server import default_format_error, encode_execution_results, json_encode
from ..templates import GRAPHIQL
class GraphQLView:
def __init__(self, *, api, schema):
self.api = api
self.schema = schema
@staticmethod
async def _resolve_graphql_query(req):
# TODO: Get variables and operation_name from form data, params, request text?
if "json" in req.mimetype:
json_media = await req.media("json")
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
if "q" in req.params:
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
async def graphql_response(self, req, resp, schema):
show_graphiql = req.method == "get" and req.accepts("text/html")
if show_graphiql:
resp.content = self.api.template_string(GRAPHIQL, endpoint=req.url.path)
return
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(
query, variables=variables, operation_name=operation_name
)
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)
async def on_request(self, req, resp):
await self.graphql_response(req, resp, self.schema)
+138
View File
@@ -0,0 +1,138 @@
from pathlib import Path
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from responder import status_codes
from responder.statics import API_THEMES, DEFAULT_OPENAPI_THEME
from responder.templates import Templates
class OpenAPISchema:
def __init__(
self,
app,
title,
version,
plugins=None,
description=None,
terms_of_service=None,
contact=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 = {}
self.title = title
self.version = version
self.description = description
self.terms_of_service = terms_of_service
self.contact = contact
self.license = license
self.openapi_version = openapi
self.openapi_route = openapi_route
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
if self.openapi_version is not None:
self.app.add_route(self.openapi_route, self.schema_response)
if self.docs_route is not None:
self.app.add_route(self.docs_route, self.docs_response)
theme_path = (Path(__file__).parent / "docs").resolve()
self.templates = Templates(directory=theme_path)
self.static_route = static_route
@property
def _apispec(self):
info = {}
if self.description is not None:
info["description"] = self.description
if self.terms_of_service is not None:
info["termsOfService"] = self.terms_of_service
if self.contact is not None:
info["contact"] = self.contact
if self.license is not None:
info["license"] = self.license
spec = APISpec(
title=self.title,
version=self.version,
openapi_version=self.openapi_version,
plugins=self.plugins,
info=info,
)
for route in self.app.router.routes:
if 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():
spec.components.schema(name, schema=schema)
return spec
@property
def openapi(self):
return self._apispec.to_yaml()
def add_schema(self, name, schema, check_existing=True):
"""Adds a marshmallow schema to the API specification."""
if check_existing:
assert name not in self.schemas
self.schemas[name] = schema
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):
name = fields.Str()
"""
def decorator(f):
self.add_schema(name=name, schema=f, **options)
return f
return decorator
@property
def docs(self):
return self.templates.render(
f"{self.docs_theme}.html",
title=self.title,
version=self.version,
schema_url="/schema.yml",
)
def static_url(self, asset):
"""Given a static asset, return its URL path."""
assert self.static_route is not None
return f"{self.static_route}/{str(asset)}"
def docs_response(self, req, resp):
resp.html = self.docs
def schema_response(self, req, resp):
resp.status_code = status_codes.HTTP_200
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@next/bundles/redoc.standalone.js"></script>
</body>
</html>
+49
View File
@@ -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>
+50 -29
View File
@@ -1,59 +1,80 @@
from urllib.parse import parse_qs
import json
from urllib.parse import urlencode
import yaml
import json
from parse import findall
from .models import QueryDict
from requests_toolbelt.multipart import decoder
from .models import QueryDict
async def format_form(r, encode=False):
if encode:
pass
else:
return QueryDict(await r.text)
return None
if "multipart/form-data" in r.headers.get("Content-Type"):
decode = decoder.MultipartDecoder(await r.content, r.mimetype)
queries = []
for part in decode.parts:
header = part.headers.get(b"Content-Disposition").decode("utf-8")
text = part.text
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[1]
key = key[1:-1]
queries.append((key, text))
content = urlencode(queries)
return QueryDict(content)
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")
filename = None
return None
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
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
if key == "filename":
filename = value
elif key == "name":
formname = value
if filename:
dump[filename] = part.content
return dump
if mimetype is None:
dump[formname] = part.content
else:
dump[formname] = {
"filename": filename,
"content": part.content,
"content-type": mimetype.decode("utf-8"),
}
return dump
def get_formats():
+155 -68
View File
@@ -1,24 +1,24 @@
import io
import json
import gzip
from base64 import b64decode
import functools
import inspect
import typing as t
from http.cookies import SimpleCookie
from urllib.parse import parse_qs
import chardet
import rfc3986
import graphene
import yaml
from requests.structures import CaseInsensitiveDict
from requests.cookies import RequestsCookieJar
from starlette.datastructures import MutableHeaders
from requests.structures import CaseInsensitiveDict
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
from starlette.requests import State
from starlette.responses import (
Response as StarletteResponse,
)
from starlette.responses import (
StreamingResponse as StarletteStreamingResponse,
)
from urllib.parse import parse_qs
from .status_codes import HTTP_200
from .statics import DEFAULT_ENCODING
from .status_codes import HTTP_301
class QueryDict(dict):
@@ -89,13 +89,20 @@ class QueryDict(dict):
yield from super().items()
# TODO: add slots
class Request:
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api", "_content"]
__slots__ = [
"_starlette",
"formats",
"_headers",
"_encoding",
"api",
"_content",
"_cookies",
]
def __init__(self, scope, receive, api=None):
def __init__(self, scope, receive, api=None, formats=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = None
self.formats = formats
self._encoding = None
self.api = api
self._content = None
@@ -105,18 +112,12 @@ class Request:
headers[key] = value
self._headers = headers
self._cookies = None
@property
def session(self):
"""The session data, in dict form, from the Request."""
if "Responder-Session" in self.cookies:
data = self.cookies[self.api.session_cookie]
data = self.api._signer.unsign(data)
data = b64decode(data)
return json.loads(data)
return {}
return self._starlette.session
@property
def headers(self):
@@ -145,14 +146,17 @@ class Request:
@property
def cookies(self):
"""The cookies sent in the Request, as a dictionary."""
cookies = RequestsCookieJar()
cookie_header = self.headers.get("Cookie", "")
if self._cookies is None:
cookies = RequestsCookieJar()
cookie_header = self.headers.get("Cookie", "")
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
cookies[k] = v
bc = SimpleCookie(cookie_header)
for key, morsel in bc.items():
cookies[key] = morsel.value
return cookies.get_dict()
self._cookies = cookies.get_dict()
return self._cookies
@property
def params(self):
@@ -162,6 +166,19 @@ class Request:
except AttributeError:
return QueryDict({})
@property
def state(self) -> State:
"""
Use the state to store additional information.
This can be a very helpful feature, if you want to hand over
information from a middelware or a route decorator to the
actual route handler.
Usage: ``request.state.time_started = time.time()``
"""
return self._starlette.state
@property
async def encoding(self):
"""The encoding of the Request's body. Can be set, manually. Must be awaited."""
@@ -169,13 +186,7 @@ class Request:
if self._encoding:
return self._encoding
# Then try what's defined by the Request.
elif await self.declared_encoding:
return self.declared_encoding
# Then, automatically detect the encoding.
else:
return await self.apparent_encoding
return await self.apparent_encoding
@encoding.setter
def encoding(self, value):
@@ -197,6 +208,7 @@ class Request:
async def declared_encoding(self):
if "Encoding" in self.headers:
return self.headers["Encoding"]
return None
@property
async def apparent_encoding(self):
@@ -205,8 +217,8 @@ class Request:
if declared_encoding:
return declared_encoding
else:
return chardet.detect(await self.content)["encoding"]
return chardet.detect(await self.content)["encoding"] or DEFAULT_ENCODING
@property
def is_secure(self):
@@ -216,27 +228,37 @@ 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: t.Union[str, t.Callable] = None): # noqa: A001, 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
if format in self.formats:
return await self.formats[format](self)
else:
return await format(self)
return await format(self)
def content_setter(mimetype):
def getter(instance):
return instance.content
def setter(instance, value):
instance.content = value
instance.mimetype = mimetype
return property(fget=getter, fset=setter)
class Response:
__slots__ = [
"req",
"status_code",
"text",
"content",
"encoding",
"media",
@@ -244,37 +266,62 @@ class Response:
"formats",
"cookies",
"session",
"mimetype",
"_stream",
]
text = content_setter("text/plain")
html = content_setter("text/html")
def __init__(self, req, *, formats):
self.req = req
self.status_code = None #: The HTTP Status Code to use for the Response.
self.text = None #: A unicode representation of the response body.
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.headers = (
{}
) #: A Python dictionary of ``{key: value}``, representing the headers of the response.
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.formats = formats
self.cookies = {} #: The cookies set in the Response, as a dictionary
self.cookies = SimpleCookie() #: The cookies set in the Response
self.session = (
req.session.copy()
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)
self._stream = functools.partial(func, *args, **kwargs)
return func
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
self.status_code = status_code
if set_text:
self.text = f"Redirecting to: {location}"
self.headers.update({"Location": location})
@property
async def body(self):
if self.content:
return (self.content, {})
if self._stream is not None:
return (self._stream(), {})
if self.text:
return (self.text.encode(self.encoding), {"Encoding": self.encoding})
if self.content is not None:
headers = {}
content = self.content
if self.mimetype is not None:
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)
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 (
@@ -282,12 +329,52 @@ class Response:
{"Content-Type": "application/json"},
)
async def __call__(self, receive, send):
def set_cookie(
self,
key,
value="",
expires=None,
path="/",
domain=None,
max_age=None,
secure=False,
httponly=True,
):
self.cookies[key] = value
morsel = self.cookies[key]
if expires is not None:
morsel["expires"] = expires
if path is not None:
morsel["path"] = path
if domain is not None:
morsel["domain"] = domain
if max_age is not None:
morsel["max-age"] = max_age
morsel["secure"] = secure
morsel["httponly"] = httponly
def _prepare_cookies(self, starlette_response):
cookie_header = (
(b"set-cookie", morsel.output(header="").lstrip().encode("latin-1"))
for morsel in self.cookies.values()
)
starlette_response.raw_headers.extend(cookie_header)
async def __call__(self, scope, receive, send):
body, headers = await self.body
if self.headers:
headers.update(self.headers)
response = StarletteResponse(
body, status_code=self.status_code, headers=headers
)
await response(receive, send)
if self._stream is not None:
response_cls = StarletteStreamingResponse
else:
response_cls = StarletteResponse
response = response_cls(body, status_code=self.status_code, headers=headers)
self._prepare_cookies(response)
await response(scope, receive, send)
@property
def ok(self):
return 200 <= self.status_code < 300
+323 -44
View File
@@ -1,37 +1,74 @@
import asyncio
import inspect
import re
from parse import parse
import traceback
from collections import defaultdict
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.middleware.wsgi import WSGIMiddleware
from starlette.websockets import WebSocket, WebSocketClose
from . import status_codes
from .formats import get_formats
from .models import Request, Response
_CONVERTORS = {
"int": (int, r"\d+"),
"str": (str, r"[^/]+"),
"float": (float, r"\d+(.\d+)?"),
}
PARAM_RE = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")
def memoize(f):
def helper(self, s):
memoize_key = f"{f.__name__}:{s}"
if memoize_key not in self._memo:
self._memo[memoize_key] = f(self, s)
return self._memo[memoize_key]
def compile_path(path):
path_re = "^"
param_convertors = {}
idx = 0
return helper
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}'"
convertor, convertor_re = _CONVERTORS[convertor_type]
path_re += path[idx : match.start()]
path_re += rf"(?P<{param_name}>{convertor_re})"
param_convertors[param_name] = convertor
idx = match.end()
path_re += path[idx:] + "$"
return re.compile(path_re), param_convertors
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
class BaseRoute:
def matches(self, scope):
raise NotImplementedError()
def __init__(self, route, endpoint, *, websocket=False, before_request=False):
async def __call__(self, scope, receive, send):
raise NotImplementedError()
class Route(BaseRoute):
def __init__(self, route, endpoint, *, before_request=False):
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
self.endpoint = endpoint
self.uses_websocket = websocket
self.before_request = before_request
self._memo = {}
self.path_re, self.param_convertors = compile_path(route)
def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>"
def __eq__(self, other):
if hasattr(other, "route"):
# Being compared to other routes.
return self.route == other.route
else:
# Strings.
return self.does_match(other)
def url(self, **params):
return self.route.format(**params)
@property
def endpoint_name(self):
@@ -41,37 +78,279 @@ class Route:
def description(self):
return self.endpoint.__doc__
@property
def has_parameters(self):
return bool(self._param_pattern.search(self.route))
def matches(self, scope):
if scope["type"] != "http":
return False, {}
@memoize
def does_match(self, s):
if s == self.route:
return True
path = scope["path"]
match = self.path_re.match(path)
named = self.incoming_matches(s)
return bool(len(named))
if match is None:
return False, {}
@memoize
def incoming_matches(self, s):
results = parse(self.route, s)
return results.named if results else {}
matched_params = match.groupdict()
for key, value in matched_params.items():
matched_params[key] = self.param_convertors[key](value)
return True, {"path_params": {**matched_params}}
async def __call__(self, scope, receive, send):
request = Request(scope, receive, formats=get_formats())
response = Response(req=request, formats=get_formats())
path_params = scope.get("path_params", {})
before_requests = scope.get("before_requests", [])
for before_request in before_requests.get("http", []):
if asyncio.iscoroutinefunction(before_request):
await before_request(request, response)
else:
await run_in_threadpool(before_request, request, response)
views = []
if inspect.isclass(self.endpoint):
endpoint = self.endpoint()
on_request = getattr(endpoint, "on_request", None)
if on_request:
views.append(on_request)
method_name = f"on_{request.method}"
try:
view = getattr(endpoint, method_name)
views.append(view)
except AttributeError as ex:
if on_request is None:
raise HTTPException(status_code=status_codes.HTTP_405) from ex
else:
views.append(self.endpoint)
for view in views:
# "Monckey patch" for graphql: explicitly checking __call__
if asyncio.iscoroutinefunction(view) or asyncio.iscoroutinefunction(
view.__call__
):
await view(request, response, **path_params)
else:
await run_in_threadpool(view, request, response, **path_params)
if response.status_code is None:
response.status_code = status_codes.HTTP_200
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):
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
class WebSocketRoute(BaseRoute):
def __init__(self, route, endpoint, *, before_request=False):
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
self.endpoint = endpoint
self.before_request = before_request
self.path_re, self.param_convertors = compile_path(route)
def __repr__(self):
return f"<Route {self.route!r}={self.endpoint!r}>"
def url(self, **params):
return self.route.format(**params)
def _weight(self):
params = set(self._param_pattern.findall(self.route))
params_count = len(params)
return params_count != 0, -params_count
@property
def endpoint_name(self):
return self.endpoint.__name__
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
def description(self):
return self.endpoint.__doc__
@property
def is_function(self):
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((callable(self.endpoint), code, kwdefaults))
def matches(self, scope):
if scope["type"] != "websocket":
return False, {}
path = scope["path"]
match = self.path_re.match(path)
if match is None:
return False, {}
matched_params = match.groupdict()
for key, value in matched_params.items():
matched_params[key] = self.param_convertors[key](value)
return True, {"path_params": {**matched_params}}
async def __call__(self, scope, receive, send):
ws = WebSocket(scope, receive, send)
before_requests = scope.get("before_requests", [])
for before_request in before_requests.get("ws", []):
await before_request(ws)
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):
return hash(self.route) ^ hash(self.endpoint) ^ hash(self.before_request)
class Router:
def __init__(self, routes=None, default_response=None, before_requests=None):
self.routes = [] if routes is None else list(routes)
# [TODO] Make its own router
self.apps = {}
self.default_endpoint = (
self.default_response if default_response is None else default_response
)
self.before_requests = (
{"http": [], "ws": []} if before_requests is None else before_requests
)
self.events = defaultdict(list)
def add_route(
self,
route=None,
endpoint=None,
*,
default=False,
websocket=False,
before_request=False,
check_existing=False,
):
"""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.
"""
if before_request:
if websocket:
self.before_requests.setdefault("ws", []).append(endpoint)
else:
self.before_requests.setdefault("http", []).append(endpoint)
return
if check_existing:
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
if websocket:
route = WebSocketRoute(route, endpoint)
else:
route = Route(route, endpoint)
self.routes.append(route)
def mount(self, 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:
self.before_requests.setdefault("ws", []).append(endpoint)
else:
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)
return None
async def default_response(self, scope, receive, send):
if scope["type"] == "websocket":
websocket_close = WebSocketClose()
await websocket_close(receive, send)
return
# FIXME: Please review!
request = Request(scope, receive)
response = Response(request, formats=get_formats()) # noqa: F841
raise HTTPException(status_code=status_codes.HTTP_404)
def _resolve_route(self, scope):
for route in self.routes:
matches, child_scope = route.matches(scope)
if matches:
scope.update(child_scope)
return route
return None
async def lifespan(self, scope, receive, send):
message = await receive()
assert message["type"] == "lifespan.startup"
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(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):
scope["path"] = path[len(path_prefix) :]
scope["root_path"] = root_path + path_prefix
try:
await app(scope, receive, send)
return
except TypeError:
app = WSGIMiddleware(app)
await app(scope, receive, send)
return
await self.default_endpoint(scope, receive, send)
+20
View File
@@ -0,0 +1,20 @@
from starlette.staticfiles import StaticFiles as StarletteStaticFiles
class StaticFiles(StarletteStaticFiles):
"""
Extension to Starlette's `StaticFiles`.
I've created an issue to discuss allowing multiple directories in
Starlette's `StaticFiles`.
https://github.com/encode/starlette/issues/625
I've also made a PR to add this method to Starlette StaticFiles
Once accepted we will remove this.
https://github.com/encode/starlette/pull/626
"""
def add_directory(self, directory: str) -> None:
self.all_directories = [*self.all_directories, *self.get_directories(directory)]
+10 -9
View File
@@ -1,14 +1,15 @@
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": (),
"allow_methods": ("GET",),
"allow_headers": (),
"allow_credentials": False,
"allow_origin_regex": None,
"expose_headers": (),
"max_age": 600,
"allow_origins": (),
"allow_methods": ("GET",),
"allow_headers": (),
"allow_credentials": False,
"allow_origin_regex": None,
"expose_headers": (),
"max_age": 600,
}
+59
View File
@@ -0,0 +1,59 @@
from contextlib import contextmanager
import jinja2
class Templates:
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, # noqa: S701
enable_async=enable_async,
)
self.default_context = {} if context is None else {**context}
self._env.globals.update(self.default_context)
@property
def context(self):
return self._env.globals
@context.setter
def context(self, context):
self._env.globals = {**self.default_context, **context}
def get_template(self, name):
return self._env.get_template(name)
def render(self, template, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
: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
def _async(self):
self._env.is_async = True
try:
yield
finally:
self._env.is_async = False
async def render_async(self, template, *args, **kwargs):
with self._async():
return await self.get_template(template).render_async(*args, **kwargs)
def render_string(self, source, *args, **kwargs):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template string, with provided values supplied.
: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)
-145
View File
@@ -1,145 +0,0 @@
GRAPHIQL = """
{% set GRAPHIQL_VERSION = '0.12.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>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#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>
</head>
<body>
<div id="graphiql">Loading...</div>
<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')
);
</script>
</body>
</html>
""".strip()
+28 -119
View File
@@ -2,156 +2,65 @@
# -*- coding: utf-8 -*-
import codecs
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
from setuptools import find_packages, setup
from versioningit import get_cmdclasses
here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
long_description = "\n" + f.read()
about = {}
with open(os.path.join(here, "responder", "__version__.py")) as f:
exec(f.read(), about)
if sys.argv[-1] == "publish":
os.system("python setup.py sdist bdist_wheel upload")
sys.exit()
required = [
"starlette",
"uvicorn",
"aiofiles",
"pyyaml",
"requests",
"graphene",
"graphql-server-core>=1.1",
"jinja2",
"parse",
"uvloop ; sys_platform != 'win32'",
"rfc3986",
"python-multipart",
"chardet",
"apispec>=1.0.0b1",
"chardet",
"docopt-ng",
"marshmallow",
"asgiref",
"whitenoise",
"docopt",
"itsdangerous",
"requests",
"requests-toolbelt",
"apistar",
"rfc3986",
"starlette[full]",
"uvicorn[standard]",
"whitenoise",
]
# https://pypi.python.org/pypi/stdeb/0.8.5#quickstart-2-just-tell-me-the-fastest-way-to-make-a-deb
class DebCommand(Command):
"""Support for setup.py deb"""
description = "Build and publish the .deb package."
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print("\033[1m{0}\033[0m".format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status("Removing previous builds…")
rmtree(os.path.join(here, "deb_dist"))
except FileNotFoundError:
pass
self.status(u"Creating debian mainfest…")
os.system(
"python setup.py --command-packages=stdeb.command sdist_dsc -z artful --package3=pipenv --depends3=python3-virtualenv-clone"
)
self.status(u"Building .deb…")
os.chdir("deb_dist/pipenv-{0}".format(about["__version__"]))
os.system("dpkg-buildpackage -rfakeroot -uc -us")
class UploadCommand(Command):
"""Support setup.py publish."""
description = "Build and publish the package."
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print("\033[1m{0}\033[0m".format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status("Removing previous builds…")
rmtree(os.path.join(here, "dist"))
except FileNotFoundError:
pass
self.status("Building Source distribution…")
os.system("{0} setup.py sdist bdist_wheel".format(sys.executable))
self.status("Uploading the package to PyPI via Twine…")
os.system("twine upload dist/*")
self.status("Pushing git tags…")
os.system("git tag v{0}".format(about["__version__"]))
os.system("git push --tags")
sys.exit()
setup(
name="responder",
version=about["__version__"],
description="A sorta familiar HTTP framework.",
description="A familiar HTTP Service Framework for Python.",
long_description=long_description,
long_description_content_type="text/markdown",
author="Kenneth Reitz",
author_email="me@kennethreitz.org",
url="https://github.com/kennethreitz/responder",
packages=find_packages(exclude=["tests"]),
entry_points={"console_scripts": ["responder=responder.cli:cli"]},
package_data={
# "": ["LICENSE", "NOTICES"],
# "pipenv.vendor.requests": ["*.pem"],
# "pipenv.vendor.certifi": ["*.pem"],
# "pipenv.vendor.click_completion": ["*.j2"],
# "pipenv.patched.notpip._vendor.certifi": ["*.pem"],
# "pipenv.patched.notpip._vendor.requests": ["*.pem"],
# "pipenv.patched.notpip._vendor.distlib._backport": ["sysconfig.cfg"],
# "pipenv.patched.notpip._vendor.distlib": [
# "t32.exe",
# "t64.exe",
# "w32.exe",
# "w64.exe",
# ],
},
python_requires=">=3.6",
package_data={},
python_requires=">=3.10",
setup_requires=[],
install_requires=required,
extras_require={},
extras_require={
"develop": ["poethepoet", "pyproject-fmt", "ruff", "validate-pyproject"],
"graphql": ["graphene"],
"release": ["build", "twine"],
"test": ["pytest", "pytest-cov", "pytest-mock", "flask"],
},
include_package_data=True,
license="Apache 2.0",
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.6",
"Programming Language :: Python :: 3.7",
"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",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet :: WWW/HTTP",
],
cmdclass={"upload": UploadCommand, "deb": DebCommand},
cmdclass=get_cmdclasses(),
)
+11 -13
View File
@@ -1,9 +1,8 @@
import graphene
import responder
from pathlib import Path
import pytest
import multiprocessing
import concurrent.futures
import responder
@pytest.fixture
@@ -18,7 +17,7 @@ def current_dir():
@pytest.fixture
def api():
return responder.API()
return responder.API(debug=False, allowed_hosts=[";"])
@pytest.fixture
@@ -46,12 +45,11 @@ def flask():
return app
@pytest.fixture
def schema():
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
return graphene.Schema(query=Query)
def template_path(tmpdir):
# create a Jinja template file on the filesystem
template_name = "test.html"
template_file = tmpdir.mkdir("static").join(template_name)
template_file.write("{{ var }}")
return template_file
+2 -5
View File
@@ -1,6 +1,3 @@
import pytest
def test_custom_encoding(api, session):
data = "hi alex!"
@@ -9,7 +6,7 @@ def test_custom_encoding(api, session):
req.encoding = "ascii"
resp.text = await req.text
r = session.get(api.url_for(route), data=data)
r = session.post(api.url_for(route), data=data)
assert r.text == data
@@ -20,5 +17,5 @@ def test_bytes_encoding(api, session):
async def route(req, resp):
resp.text = (await req.content).decode("utf-8")
r = session.get(api.url_for(route), data=data)
r = session.post(api.url_for(route), data=data)
assert r.content == data
+1 -1
View File
@@ -1,9 +1,9 @@
import inspect
import pytest
from responder import models
_default_query = "q=%7b%20hello%20%7d&name=myname&user_name=test_user"
+566 -135
View File
@@ -1,12 +1,14 @@
import concurrent
import random
import string
import pytest
import yaml
import responder
import requests
import io
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.testclient import TestClient as StarletteTestClient
from starlette.responses import PlainTextResponse
import responder
from responder.routes import Route, WebSocketRoute
from responder.templates import Templates
def test_api_basic_route(api):
@@ -15,6 +17,45 @@ def test_api_basic_route(api):
resp.text = "hello world!"
def test_route_repr():
def home(req, resp):
"""Home page"""
resp.text = "Hello !"
route = Route("/", home)
assert route.__repr__() == f"<Route '/'={home!r}>"
assert route.endpoint_name == home.__name__
assert route.description == home.__doc__
def test_websocket_route_repr():
def chat_endpoint(ws):
"""Chat"""
pass
route = WebSocketRoute("/", chat_endpoint)
assert route.__repr__() == f"<Route '/'={chat_endpoint!r}>"
assert route.endpoint_name == chat_endpoint.__name__
assert route.description == chat_endpoint.__doc__
def test_route_eq():
def home(req, resp):
resp.text = "Hello !"
assert Route("/", home) == Route("/", home)
def chat(ws):
pass
assert WebSocketRoute("/", home) == WebSocketRoute("/", home)
"""
def test_api_basic_route_overlap(api):
@api.route("/")
def home(req, resp):
@@ -25,39 +66,7 @@ def test_api_basic_route_overlap(api):
@api.route("/")
def home2(req, resp):
resp.text = "hello world!"
def test_api_basic_route_overlap_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
with pytest.raises(AssertionError):
api.add_route("/", home2)
def test_api_basic_route_overlap_allowed(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
def home2(req, resp):
resp.text = "hello world!"
api.add_route("/", home2, check_existing=False)
def test_api_basic_route_overlap_allowed_alternative(api):
@api.route("/")
def home(req, resp):
resp.text = "hello world!"
@api.route("/", check_existing=False)
def home2(req, resp):
resp.text = "hello world!"
"""
def test_class_based_view_registration(api):
@@ -70,10 +79,10 @@ def test_class_based_view_registration(api):
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(self, req, resp, *, greeting):
resp.text = f"{greeting}, world!"
pass
assert api.session().get("http://;/Hello").ok
resp = api.session().get("http://;/Hello")
assert resp.status_code == api.status_codes.HTTP_405
def test_requests_session(api):
@@ -81,14 +90,14 @@ def test_requests_session(api):
assert api.requests
def test_requests_session_works(api, url):
def test_requests_session_works(api):
TEXT = "spiral out"
@api.route("/")
def hello(req, resp):
resp.text = TEXT
assert api.requests.get(url("/")).text == TEXT
assert api.requests.get("/").text == TEXT
def test_status_code(api):
@@ -123,14 +132,7 @@ def test_yaml_media(api):
r = api.requests.get("http://;/", headers={"Accept": "yaml"})
assert "yaml" in r.headers["Content-Type"]
assert yaml.load(r.content) == dump
def test_graphql_schema_query_querying(api, schema):
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"})
assert r.json() == {"data": {"hello": "Hello stranger"}}
assert yaml.load(r.content, Loader=yaml.FullLoader) == dump # noqa: S506
def test_argumented_routing(api):
@@ -187,14 +189,16 @@ def test_query_params(api, url):
# Requires https://github.com/encode/starlette/pull/102
def test_form_data(api):
@api.route("/")
async def route(req, resp):
resp.media = {"form": await req.media("form")}
# def test_form_data(api):
dump = {"q": "q"}
r = api.requests.get(api.url_for(route), data=dump)
assert r.json()["form"] == dump
# @api.route("/")
# async def route(req, resp):
# resp.media = {"form": await req.media("form")}
# dump = {"q": "q"}
# r = api.requests.get(api.url_for(route), params=dump)
# assert r.json()["form"] == dump
def test_async_function(api):
@@ -219,7 +223,7 @@ def test_media_parsing(api):
assert r.json() == dump
r = api.requests.get(api.url_for(route), headers={"Accept": "application/x-yaml"})
assert r.text == "{hello: sam}\n"
assert r.text == "hello: sam\n"
def test_background(api):
@@ -235,7 +239,7 @@ def test_background(api):
api.text = "ok"
r = api.requests.get(api.url_for(route))
assert r.ok
assert r.status_code < 300
def test_multiple_routes(api):
@@ -254,21 +258,6 @@ def test_multiple_routes(api):
assert r.text == "2"
def test_graphql_schema_json_query(api, schema):
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.requests.post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_graphiql(api, schema):
api.add_route("/", responder.ext.GraphQLView(schema=schema, api=api))
r = api.requests.get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert "GraphiQL" in r.text
def test_json_uploads(api):
@api.route("/")
async def route(req, resp):
@@ -302,6 +291,11 @@ def test_form_uploads(api):
r = api.requests.post(api.url_for(route), data=dump)
assert r.json() == dump
# requests with boundary
files = {"complicated": (None, "times")}
r = api.requests.post(api.url_for(route), files=files)
assert r.json() == {"complicated": "times"}
def test_json_downloads(api):
dump = {"testing": "123"}
@@ -310,9 +304,7 @@ def test_json_downloads(api):
def route(req, resp):
resp.media = dump
r = api.requests.get(
api.url_for(route), headers={"Content-Type": "application/json"}
)
r = api.requests.get(api.url_for(route), headers={"Content-Type": "application/json"})
assert r.json() == dump
@@ -329,15 +321,19 @@ def test_yaml_downloads(api):
assert yaml.safe_load(r.content) == dump
def test_schema_generation():
def test_schema_generation_explicit():
import marshmallow
import responder
from marshmallow import Schema, fields
from responder.ext.schema import OpenAPISchema as OpenAPISchema
api = responder.API(title="Web Service", openapi="3.0")
api = responder.API()
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
schema = OpenAPISchema(app=api, title="Web Service", version="1.0", openapi="3.0.2")
@schema.schema("Pet")
class PetSchema(marshmallow.Schema):
name = marshmallow.fields.Str()
@api.route("/")
def route(req, resp):
@@ -349,7 +345,7 @@ def test_schema_generation():
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
$ref: "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
@@ -357,14 +353,15 @@ def test_schema_generation():
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0"
assert dump["openapi"] == "3.0.2"
def test_documentation():
import responder
def test_schema_generation():
from marshmallow import Schema, fields
api = responder.API(title="Web Service", openapi="3.0", docs_route="/docs")
import responder
api = responder.API(title="Web Service", openapi="3.0.2")
@api.schema("Pet")
class PetSchema(Schema):
@@ -380,7 +377,115 @@ def test_documentation():
200:
description: A pet to be returned
schema:
$ref = "#/components/schemas/Pet"
$ref: "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.requests.get("http://;/schema.yml")
dump = yaml.safe_load(r.content)
assert dump
assert dump["openapi"] == "3.0.2"
def test_documentation_explicit():
import marshmallow
import responder
from responder.ext.schema import OpenAPISchema as OpenAPISchema
description = "This is a sample server for a pet store."
terms_of_service = "http://example.com/terms/"
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license_ = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(allowed_hosts=["testserver", ";"])
schema = OpenAPISchema(
app=api,
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license_,
)
@schema.schema("Pet")
class PetSchema(marshmallow.Schema):
name = marshmallow.fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref: "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
r = api.requests.get("/docs")
assert "html" in r.text
def test_documentation():
from marshmallow import Schema, fields
import responder
description = "This is a sample server for a pet store."
terms_of_service = "http://example.com/terms/"
contact = {
"name": "API Support",
"url": "http://www.example.com/support",
"email": "support@example.com",
}
license_ = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license_,
allowed_hosts=["testserver", ";"],
)
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
schema:
$ref: "#/components/schemas/Pet"
"""
resp.media = PetSchema().dump({"name": "little orange"})
@@ -396,7 +501,7 @@ def test_mount_wsgi_app(api, flask):
api.mount("/flask", flask)
r = api.requests.get("http://;/flask")
assert r.ok
assert r.status_code < 300
def test_async_class_based_views(api):
@@ -415,13 +520,23 @@ def test_cookies(api):
def cookies(req, resp):
resp.media = {"cookies": req.cookies}
resp.cookies["sent"] = "true"
resp.set_cookie(
"hello",
"world",
expires=123,
path="/",
max_age=123,
secure=False,
httponly=True,
)
r = api.requests.get(api.url_for(cookies), cookies={"hello": "universe"})
assert r.json() == {"cookies": {"hello": "universe"}}
assert "sent" in r.cookies
assert "hello" in r.cookies
r = api.requests.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
assert r.json() == {"cookies": {"hello": "world", "sent": "true"}}
@pytest.mark.xfail
@@ -432,17 +547,16 @@ def test_sessions(api):
resp.media = resp.session
r = api.requests.get(api.url_for(view))
assert "Responder-Session" in r.cookies
assert api.session_cookie in r.cookies
r = api.requests.get(api.url_for(view))
assert (
r.cookies["Responder-Session"]
== '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
r.cookies[api.session_cookie] == '{"hello": "world"}.r3EB04hEEyLYIJaAXCEq3d4YEbs'
)
assert r.json() == {"hello": "world"}
def test_template_rendering(api):
def test_template_string_rendering(api):
@api.route("/")
def view(req, resp):
resp.content = api.template_string("{{ var }}", var="hello")
@@ -451,33 +565,66 @@ def test_template_rendering(api):
assert r.text == "hello"
def test_template_rendering(template_path):
api = responder.API(templates_dir=template_path.dirpath())
@api.route("/")
def view(req, resp):
resp.content = api.template(template_path.basename, var="hello")
r = api.requests.get(api.url_for(view))
assert r.text == "hello"
def test_template(api, template_path):
templates = Templates(directory=template_path.dirpath())
@api.route("/{var}/")
def view(req, resp, var):
resp.html = templates.render(template_path.basename, var=var)
r = api.requests.get("/test/")
assert r.text == "test"
def test_template_async(api, template_path):
templates = Templates(directory=template_path.dirpath(), enable_async=True)
@api.route("/{var}/async")
async def view_async(req, resp, var):
resp.html = await templates.render_async(template_path.basename, var=var)
r = api.requests.get("/test/async")
assert r.text == "test"
def test_file_uploads(api):
@api.route("/")
async def upload(req, resp):
files = await req.media("files")
files["hello"] = files["hello"].decode("utf-8")
resp.media = {"files": files}
result = {}
result["hello"] = files["hello"]["content"].decode("utf-8")
# result["not-a-file"] = files["not-a-file"].decode("utf-8")
resp.media = {"files": result}
world = io.StringIO("world")
data = {"hello": world}
r = api.requests.post(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}
# # world = io.StringIO("world")
# data = {"hello": ("hello.txt", world, "text/plain"), "not-a-file": b"data only"}
# r = api.requests.post(api.url_for(upload), files=data)
# assert r.json() == {"files": {"hello": "world", "not-a-file": "data only"}}
def test_500(api):
def catcher(request, exc):
return PlainTextResponse("Suppressed error", 500)
api.app.add_exception_handler(ValueError, catcher)
@api.route("/")
def view(req, resp):
raise ValueError
r = api.requests.get(api.url_for(view))
assert not r.ok
assert r.content == b"Suppressed error"
dumb_client = responder.api.TestClient(
api, base_url="http://;", raise_server_exceptions=False
)
r = dumb_client.get(api.url_for(view))
assert r.status_code >= 300
assert r.status_code == responder.status_codes.HTTP_500
def test_404(api):
@@ -486,16 +633,73 @@ def test_404(api):
assert r.status_code == responder.status_codes.HTTP_404
def test_kinda_websockets(api):
def test_websockets_text(api):
payload = "Hello via websocket!"
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_text("Hello via websocket!")
await ws.send_text(payload)
await ws.close()
client = StarletteTestClient(api)
with client.websocket_connect("ws://;/ws") as ws:
data = ws.receive_text()
assert data == payload
@pytest.mark.xfail
def test_startup(api, session):
def test_websockets_bytes(api):
payload = b"Hello via websocket!"
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_bytes(payload)
await ws.close()
client = StarletteTestClient(api)
with client.websocket_connect("ws://;/ws") as ws:
data = ws.receive_bytes()
assert data == payload
def test_websockets_json(api):
payload = {"Hello": "via websocket!"}
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
await ws.send_json(payload)
await ws.close()
client = StarletteTestClient(api)
with client.websocket_connect("ws://;/ws") as ws:
data = ws.receive_json()
assert data == payload
def test_before_websockets(api):
payload = {"Hello": "via websocket!"}
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.send_json(payload)
await ws.close()
@api.before_request(websocket=True)
async def before_request(ws):
await ws.accept()
await ws.send_json({"before": "request"})
client = StarletteTestClient(api)
with client.websocket_connect("ws://;/ws") as ws:
data = ws.receive_json()
assert data == {"before": "request"}
data = ws.receive_json()
assert data == payload
def test_startup(api):
who = [None]
@api.route("/{greeting}")
@@ -503,19 +707,12 @@ def test_startup(api, session):
resp.text = f"{greeting}, {who[0]}!"
@api.on_event("startup")
async def asd():
async def run_startup():
who[0] = "world"
print("startup")
@api.on_event("cleanup")
async def asd():
print("cleanup")
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
f = pool.submit(api.run)
r = requests.get(f"http://localhost:5042/hello")
assert r.text == "hello, world!"
with api.requests as session:
r = session.get("http://;/hello")
assert r.text == "hello, world!"
def test_redirects(api, session):
@@ -527,35 +724,269 @@ def test_redirects(api, session):
def one(req, resp):
resp.text = "redirected"
assert session.get("/1").url == "http://testserver/1"
assert session.get("/2").url == "http://;/1"
def test_session_thoroughly(api, session):
@api.route("/set")
def set(req, resp):
def setter(req, resp):
resp.session["hello"] = "world"
api.redirect(resp, location="/get")
@api.route("/get")
def get(req, resp):
def getter(req, resp):
resp.media = {"session": req.session}
r = session.get(api.url_for(set))
print(r.headers)
r = session.get(api.url_for(get))
print(r.request.headers)
r = session.get(api.url_for(setter))
r = session.get(api.url_for(getter))
assert r.json() == {"session": {"hello": "world"}}
def test_before_responpse(api, session):
def test_before_response(api, session):
@api.route("/get")
def get(req, resp):
resp.media = req.session
@api.route(before_request=True)
async def async_before_request(req, resp):
resp.headers["x-pizza"] = "1"
@api.route(before_request=True)
def before_request(req, resp):
resp.headers["x-pizza"] = "1"
r = session.get(api.url_for(get))
assert 'x-pizza' in r.headers
assert "x-pizza" in r.headers
@pytest.mark.parametrize("enable_hsts", [True, False])
@pytest.mark.parametrize("cors", [True, False])
def test_allowed_hosts(enable_hsts, cors):
api = responder.API(
allowed_hosts=[";", "tenant.;"], enable_hsts=enable_hsts, cors=cors
)
@api.route("/")
def get(req, resp):
pass
# Exact match
r = api.requests.get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://tenant.;").get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://unkownhost").get(api.url_for(get))
assert r.status_code == 400
# Reset the session
api._session = None
r = api.session(base_url="http://unkown_tenant.;").get(api.url_for(get))
assert r.status_code == 400
api = responder.API(allowed_hosts=["*.;"])
@api.route("/")
def get(req, resp):
pass
# Wildcard domains
# Using http://;
r = api.requests.get(api.url_for(get))
assert r.status_code == 400
# Reset the session
api._session = None
r = api.session(base_url="http://tenant1.;").get(api.url_for(get))
assert r.status_code == 200
# Reset the session
api._session = None
r = api.session(base_url="http://tenant2.;").get(api.url_for(get))
assert r.status_code == 200
def create_asset(static_dir, name=None, parent_dir=None):
if name is None:
name = random.choices(string.ascii_letters, k=6) # noqa: S311
# :3
ext = random.choices(string.ascii_letters, k=2) # noqa: S311
name = f"{name}.{ext}"
if parent_dir is None:
parent_dir = static_dir
else:
parent_dir = static_dir.mkdir(parent_dir)
asset = parent_dir.join(name)
asset.write("body { color: blue; }")
return asset
@pytest.mark.parametrize("static_route", [None, "/static", "/custom/static/route"])
def test_staticfiles(tmpdir, static_route):
static_dir = tmpdir.mkdir("static")
asset1 = create_asset(static_dir)
parent_dir = "css"
asset2 = create_asset(static_dir, name="asset2", parent_dir=parent_dir)
api = responder.API(static_dir=str(static_dir), static_route=static_route)
session = api.session()
static_route = api.static_route
# ok
r = session.get(f"{static_route}/{asset1.basename}")
assert r.status_code == api.status_codes.HTTP_200
r = session.get(f"{static_route}/{parent_dir}/{asset2.basename}")
assert r.status_code == api.status_codes.HTTP_200
# Asset not found
r = session.get(f"{static_route}/not_found.css")
assert r.status_code == api.status_codes.HTTP_404
# Not found on dir listing
r = session.get(f"{static_route}")
assert r.status_code == api.status_codes.HTTP_404
r = session.get(f"{static_route}/{parent_dir}")
assert r.status_code == api.status_codes.HTTP_404
def test_staticfiles_none_dir(tmpdir):
api = responder.API(static_dir=None)
session = api.session()
static_dir = tmpdir.mkdir("static")
asset = create_asset(static_dir)
static_route = api.static_route
# ok
r = session.get(f"{static_route}/{asset.basename}")
assert r.status_code == api.status_codes.HTTP_404
# dir listing
r = session.get(f"{static_route}")
assert r.status_code == api.status_codes.HTTP_404
# SPA
with pytest.raises(Exception): # noqa: B017
api.add_route("/spa", static=True)
def test_response_html_property(api):
@api.route("/")
def view(req, resp):
resp.html = "<h1>Hello !</h1>"
assert resp.content == "<h1>Hello !</h1>"
assert resp.mimetype == "text/html"
r = api.requests.get(api.url_for(view))
assert r.content == b"<h1>Hello !</h1>"
assert r.headers["Content-Type"] == "text/html"
def test_response_text_property(api):
@api.route("/")
def view(req, resp):
resp.text = "<h1>Hello !</h1>"
assert resp.content == "<h1>Hello !</h1>"
assert resp.mimetype == "text/plain"
r = api.requests.get(api.url_for(view))
assert r.content == b"<h1>Hello !</h1>"
assert r.headers["Content-Type"] == "text/plain"
def test_stream(api, session):
async def shout_stream(who):
for c in who.upper():
yield c
@api.route("/{who}")
async def greeting(req, resp, *, who):
resp.stream(shout_stream, who)
r = session.get("/morocco")
assert r.text == "MOROCCO"
@api.route("/")
async def home(req, resp):
# Raise when it's not an async generator
with pytest.raises(AssertionError):
def foo():
pass
resp.stream(foo)
with pytest.raises(AssertionError):
async def foo():
pass
resp.stream(foo)
with pytest.raises(AssertionError):
def foo():
yield "oopsie"
resp.stream(foo)
def test_empty_req_text(api):
content = "It's working"
@api.route("/")
async def home(req, resp):
await req.text
resp.text = content
r = api.requests.post("/")
assert r.text == content
def test_api_request_state(api, url):
class StateMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request.state.test1 = 42
request.state.test2 = "Foo"
return await call_next(request)
api.add_middleware(StateMiddleware)
@api.route("/")
def home(req, resp):
resp.text = "{}_{}".format(req.state.test2, req.state.test1)
assert api.requests.get(url("/")).text == "Foo_42"
def test_path_matches_route(api):
@api.route("/hello")
def home(req, resp):
resp.text = "hello world!"
route = api.path_matches_route({"type": "http", "path": "/hello"})
assert route.endpoint_name == "home"
assert not api.path_matches_route({"type": "http", "path": "/foo"})
def test_route_without_endpoint(api):
# test that a route without endpoint gets a default static response
api.add_route("/")
route = api.router.routes[0]
assert route.endpoint_name == "_static_response"
-105
View File
@@ -1,105 +0,0 @@
import pytest
from responder import routes
@pytest.mark.parametrize(
"route, expected",
[
pytest.param("/", False, id="home path without params"),
pytest.param("/test_path", False, id="sub path without params"),
pytest.param("/{test_path}", True, id="path with params"),
],
)
def test_parameter(route, expected):
r = routes.Route(route, "test_endpoint")
assert r.has_parameters is expected
def test_url():
r = routes.Route("/{my_path}", "test_endpoint")
url = r.url(my_path="path")
assert url == "/path"
def test_equal():
r = routes.Route("/{path_param}", "test_endpoint")
r2 = routes.Route("/{path_param}", "test_endpoint")
r3 = routes.Route("/test_path", "test_endpoint")
assert r == r2
assert r != r3
def test_incoming_matches():
# Test Route with one param
r = routes.Route("/{greetings}", "test_endpoint")
assert r.incoming_matches("/hello") == {"greetings": "hello"}
assert r.incoming_matches("/foo") == {"greetings": "foo"}
assert r._memo == {
"incoming_matches:/hello": {"greetings": "hello"},
"incoming_matches:/foo": {"greetings": "foo"},
}
# Test Route with two params
r = routes.Route("/{greetings}/{name}", "test_endpoint")
assert r.incoming_matches("/hi/john") == {"greetings": "hi", "name": "john"}
assert r.incoming_matches("/hello/jane") == {"greetings": "hello", "name": "jane"}
# Test Route with no param
assert r._memo == {
"incoming_matches:/hi/john": {"greetings": "hi", "name": "john"},
"incoming_matches:/hello/jane": {"greetings": "hello", "name": "jane"},
}
r = routes.Route("/hello", "test_endpoint")
assert r.incoming_matches("/hello") == {}
assert r.incoming_matches("/bye") == {}
assert r._memo == {"incoming_matches:/hello": {}, "incoming_matches:/bye": {}}
def test_incoming_matches_with_concrete_path_no_match():
r = routes.Route("/concrete_path", "test_endpoint")
assert r.incoming_matches("hello") == {}
@pytest.mark.parametrize(
"route, match, expected",
[
pytest.param(
"/{path_param}",
"/{path_param}",
True,
id="with both parametrized path match",
),
pytest.param(
"/concrete", "/concrete", True, id="with both concrete path match"
),
pytest.param("/concrete", "/no_match", False, id="with no match"),
],
)
def test_does_match_with_route(route, match, expected):
r = routes.Route(route, "test_endpoint")
assert r.does_match(match) == expected
@pytest.mark.parametrize(
"path_param, expected_weight",
[
pytest.param("/{greetings}", (True, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}", (True, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}", (True, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}", (True, -3), id="with 3 param and subpath"
),
pytest.param(
"/{greetings}_{name}", (True, -2), id="with 2 param and underscore"
),
pytest.param("/hello", (False, 0), id="with 2 param and underscore"),
],
)
def test_weight(path_param, expected_weight):
r = routes.Route(path_param, "test_endpoint")
assert r._weight() == expected_weight
+9 -8
View File
@@ -1,4 +1,5 @@
import pytest
from responder import status_codes
@@ -8,7 +9,7 @@ from responder import status_codes
pytest.param(101, True, id="Normal 101"),
pytest.param(199, True, id="Not actual status code but within 100"),
pytest.param(0, False, id="Zero case (below 100)"),
pytest.param(200, False, id="Above 100")
pytest.param(200, False, id="Above 100"),
],
)
def test_is_100(status_code, expected):
@@ -21,7 +22,7 @@ def test_is_100(status_code, expected):
pytest.param(201, True, id="Normal 201"),
pytest.param(299, True, id="Not actual status code but within 200"),
pytest.param(0, False, id="Zero case (below 200)"),
pytest.param(300, False, id="Above 200")
pytest.param(300, False, id="Above 200"),
],
)
def test_is_200(status_code, expected):
@@ -34,7 +35,7 @@ def test_is_200(status_code, expected):
pytest.param(301, True, id="Normal 301"),
pytest.param(399, True, id="Not actual status code but within 300"),
pytest.param(0, False, id="Zero case (below 300)"),
pytest.param(400, False, id="Above 300")
pytest.param(400, False, id="Above 300"),
],
)
def test_is_300(status_code, expected):
@@ -47,7 +48,7 @@ def test_is_300(status_code, expected):
pytest.param(401, True, id="Normal 401"),
pytest.param(499, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(500, False, id="Above 400")
pytest.param(500, False, id="Above 400"),
],
)
def test_is_400(status_code, expected):
@@ -57,10 +58,10 @@ def test_is_400(status_code, expected):
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(501, True, id="Normal 401"),
pytest.param(599, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(600, False, id="Above 500")
pytest.param(501, True, id="Normal 501"),
pytest.param(599, True, id="Not actual status code but within 500"),
pytest.param(0, False, id="Zero case (below 500)"),
pytest.param(600, False, id="Above 500"),
],
)
def test_is_500(status_code, expected):