Import Upstream version 2.2.2
|
@ -0,0 +1,13 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
max_line_length = 88
|
||||
|
||||
[*.{yml,yaml,json,js,css,html}]
|
||||
indent_size = 2
|
|
@ -0,0 +1,8 @@
|
|||
# Normalize CRLF to LF for all text files
|
||||
* text=auto
|
||||
|
||||
# Declare binary file types so they won't be normalized
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
tests/**/*.http binary
|
||||
tests/res/test.txt binary
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a bug in Werkzeug (not other projects which depend on Werkzeug)
|
||||
---
|
||||
|
||||
<!--
|
||||
This issue tracker is a tool to address bugs in Werkzeug itself. Please
|
||||
use Pallets Discord or Stack Overflow for questions about your own code.
|
||||
|
||||
Replace this comment with a clear outline of what the bug is.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Describe how to replicate the bug.
|
||||
|
||||
Include a minimal reproducible example that demonstrates the bug.
|
||||
Include the full traceback if there was an exception.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Describe the expected behavior that should have happened but didn't.
|
||||
-->
|
||||
|
||||
Environment:
|
||||
|
||||
- Python version:
|
||||
- Werkzeug version:
|
|
@ -0,0 +1,11 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Security issue
|
||||
url: security@palletsprojects.com
|
||||
about: Do not report security issues publicly. Email our security contact.
|
||||
- name: Questions
|
||||
url: https://stackoverflow.com/questions/tagged/werkzeug?tab=Frequent
|
||||
about: Search for and ask questions about your code on Stack Overflow.
|
||||
- name: Questions and discussions
|
||||
url: https://discord.gg/pallets
|
||||
about: Discuss questions about your code on our Discord chat.
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for Werkzeug
|
||||
---
|
||||
|
||||
<!--
|
||||
Replace this comment with a description of what the feature should do.
|
||||
Include details such as links relevant specs or previous discussions.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Replace this comment with an example of the problem which this feature
|
||||
would resolve. Is this problem solvable without changes to Werkzeug,
|
||||
such as by subclassing or using an extension?
|
||||
-->
|
|
@ -0,0 +1,9 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
time: "16:00"
|
||||
timezone: "UTC"
|
|
@ -0,0 +1,30 @@
|
|||
<!--
|
||||
Before opening a PR, open a ticket describing the issue or feature the
|
||||
PR will address. Follow the steps in CONTRIBUTING.rst.
|
||||
|
||||
Replace this comment with a description of the change. Describe how it
|
||||
addresses the linked ticket.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Link to relevant issues or previous PRs, one per line. Use "fixes" to
|
||||
automatically close an issue.
|
||||
-->
|
||||
|
||||
- fixes #<issue number>
|
||||
|
||||
<!--
|
||||
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
|
||||
each box below.
|
||||
|
||||
If only docs were changed, these aren't relevant and can be removed.
|
||||
-->
|
||||
|
||||
Checklist:
|
||||
|
||||
- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
|
||||
- [ ] Add or update relevant docs, in the docs folder and in code.
|
||||
- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
|
||||
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
|
||||
- [ ] Run `pre-commit` hooks and fix any issues.
|
||||
- [ ] Run `pytest` and `tox`, no tests failed.
|
|
@ -0,0 +1,15 @@
|
|||
name: 'Lock threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: 14
|
||||
pr-inactive-days: 14
|
|
@ -0,0 +1,55 @@
|
|||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
jobs:
|
||||
tests:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
|
||||
- {name: Windows, python: '3.10', os: windows-latest, tox: py310}
|
||||
- {name: Mac, python: '3.10', os: macos-latest, tox: py310}
|
||||
- {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311}
|
||||
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||
- {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
|
||||
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'requirements/*.txt'
|
||||
- name: update pip
|
||||
run: |
|
||||
pip install -U wheel
|
||||
pip install -U setuptools
|
||||
python -m pip install -U pip
|
||||
- name: cache mypy
|
||||
uses: actions/cache@v3.0.4
|
||||
with:
|
||||
path: ./.mypy_cache
|
||||
key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
|
||||
if: matrix.tox == 'typing'
|
||||
- run: pip install tox
|
||||
- run: tox -e ${{ matrix.tox }}
|
|
@ -0,0 +1,26 @@
|
|||
MANIFEST
|
||||
build
|
||||
dist
|
||||
/src/Werkzeug.egg-info
|
||||
*.pyc
|
||||
*.pyo
|
||||
env
|
||||
.DS_Store
|
||||
docs/_build
|
||||
bench/a
|
||||
bench/b
|
||||
.tox
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage_out
|
||||
htmlcov
|
||||
.cache
|
||||
.xprocess
|
||||
.hypothesis
|
||||
test_uwsgi_failed
|
||||
.idea
|
||||
.pytest_cache/
|
||||
venv/
|
||||
.vscode
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
|
@ -0,0 +1,44 @@
|
|||
ci:
|
||||
autoupdate_branch: "2.2.x"
|
||||
autoupdate_schedule: monthly
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.37.3
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py37-plus"]
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.8.2
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
name: Reorder Python imports (src, tests)
|
||||
files: "^(?!examples/)"
|
||||
args: ["--application-directories", ".:src"]
|
||||
additional_dependencies: ["setuptools>60.9"]
|
||||
- id: reorder-python-imports
|
||||
name: Reorder Python imports (examples)
|
||||
files: "^examples/"
|
||||
args: ["--application-directories", "examples"]
|
||||
additional_dependencies: ["setuptools>60.9"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-implicit-str-concat
|
||||
- repo: https://github.com/peterdemin/pip-compile-multi
|
||||
rev: v2.4.6
|
||||
hooks:
|
||||
- id: pip-compile-multi-verify
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: fix-byte-order-marker
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
exclude: "^tests/.*.http$"
|
|
@ -0,0 +1,13 @@
|
|||
version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements/docs.txt
|
||||
- method: pip
|
||||
path: .
|
||||
sphinx:
|
||||
builder: dirhtml
|
||||
fail_on_warning: true
|
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at report@palletsprojects.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
|
@ -0,0 +1,222 @@
|
|||
How to contribute to Werkzeug
|
||||
=============================
|
||||
|
||||
Thank you for considering contributing to Werkzeug!
|
||||
|
||||
|
||||
Support questions
|
||||
-----------------
|
||||
|
||||
Please don't use the issue tracker for this. The issue tracker is a
|
||||
tool to address bugs and feature requests in Werkzeug itself. Use one of
|
||||
the following resources for questions about using Werkzeug or issues
|
||||
with your own code:
|
||||
|
||||
- The ``#get-help`` channel on our Discord chat:
|
||||
https://discord.gg/pallets
|
||||
- The mailing list flask@python.org for long term discussion or larger
|
||||
issues.
|
||||
- Ask on `Stack Overflow`_. Search with Google first using:
|
||||
``site:stackoverflow.com werkzeug {search term, exception message, etc.}``
|
||||
|
||||
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/werkzeug?tab=Frequent
|
||||
|
||||
|
||||
Reporting issues
|
||||
----------------
|
||||
|
||||
Include the following information in your post:
|
||||
|
||||
- Describe what you expected to happen.
|
||||
- If possible, include a `minimal reproducible example`_ to help us
|
||||
identify the issue. This also helps check that the issue is not with
|
||||
your own code.
|
||||
- Describe what actually happened. Include the full traceback if there
|
||||
was an exception.
|
||||
- List your Python and Werkzeug versions. If possible, check if this
|
||||
issue is already fixed in the latest releases or the latest code in
|
||||
the repository.
|
||||
|
||||
.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example
|
||||
|
||||
|
||||
Submitting patches
|
||||
------------------
|
||||
|
||||
If there is not an open issue for what you want to submit, prefer
|
||||
opening one for discussion before working on a PR. You can work on any
|
||||
issue that doesn't have an open PR linked to it or a maintainer assigned
|
||||
to it. These show up in the sidebar. No need to ask if you can work on
|
||||
an issue that interests you.
|
||||
|
||||
Include the following in your patch:
|
||||
|
||||
- Use `Black`_ to format your code. This and other tools will run
|
||||
automatically if you install `pre-commit`_ using the instructions
|
||||
below.
|
||||
- Include tests if your patch adds or changes code. Make sure the test
|
||||
fails without your patch.
|
||||
- Update any relevant docs pages and docstrings. Docs pages and
|
||||
docstrings should be wrapped at 72 characters.
|
||||
- Add an entry in ``CHANGES.rst``. Use the same style as other
|
||||
entries. Also include ``.. versionchanged::`` inline changelogs in
|
||||
relevant docstrings.
|
||||
|
||||
.. _Black: https://black.readthedocs.io
|
||||
.. _pre-commit: https://pre-commit.com
|
||||
|
||||
|
||||
First time setup
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- Download and install the `latest version of git`_.
|
||||
- Configure git with your `username`_ and `email`_.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git config --global user.name 'your name'
|
||||
$ git config --global user.email 'your email'
|
||||
|
||||
- Make sure you have a `GitHub account`_.
|
||||
- Fork Werkzeug to your GitHub account by clicking the `Fork`_ button.
|
||||
- `Clone`_ the main repository locally.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git clone https://github.com/pallets/werkzeug
|
||||
$ cd werkzeug
|
||||
|
||||
- Add your fork as a remote to push your work to. Replace
|
||||
``{username}`` with your username. This names the remote "fork", the
|
||||
default Pallets remote is "origin".
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git remote add fork https://github.com/{username}/werkzeug
|
||||
|
||||
- Create a virtualenv.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python3 -m venv env
|
||||
$ . env/bin/activate
|
||||
|
||||
On Windows, activating is different.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
> env\Scripts\activate
|
||||
|
||||
- Upgrade pip and setuptools.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python -m pip install --upgrade pip setuptools
|
||||
|
||||
- Install the development dependencies, then install Werkzeug in
|
||||
editable mode.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install -r requirements/dev.txt && pip install -e .
|
||||
|
||||
- Install the pre-commit hooks.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pre-commit install
|
||||
|
||||
.. _latest version of git: https://git-scm.com/downloads
|
||||
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
|
||||
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
|
||||
.. _GitHub account: https://github.com/join
|
||||
.. _Fork: https://github.com/pallets/werkzeug/fork
|
||||
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||
|
||||
|
||||
Start coding
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- Create a branch to identify the issue you would like to work on. If
|
||||
you're submitting a bug or documentation fix, branch off of the
|
||||
latest ".x" branch.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git fetch origin
|
||||
$ git checkout -b your-branch-name origin/2.0.x
|
||||
|
||||
If you're submitting a feature addition or change, branch off of the
|
||||
"main" branch.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git fetch origin
|
||||
$ git checkout -b your-branch-name origin/main
|
||||
|
||||
- Using your favorite editor, make your changes,
|
||||
`committing as you go`_.
|
||||
- Include tests that cover any code changes you make. Make sure the
|
||||
test fails without your patch. Run the tests as described below.
|
||||
- Push your commits to your fork on GitHub and
|
||||
`create a pull request`_. Link to the issue being addressed with
|
||||
``fixes #123`` in the pull request.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git push --set-upstream fork your-branch-name
|
||||
|
||||
.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
|
||||
.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
|
||||
|
||||
Running the tests
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run the basic test suite with pytest.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pytest
|
||||
|
||||
This runs the tests for the current environment, which is usually
|
||||
sufficient. CI will run the full suite when you submit your pull
|
||||
request. You can run the full test suite with tox if you don't want to
|
||||
wait.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ tox
|
||||
|
||||
|
||||
Running test coverage
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Generating a report of lines that do not have test coverage can indicate
|
||||
where to start contributing. Run ``pytest`` using ``coverage`` and
|
||||
generate a report.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install coverage
|
||||
$ coverage run -m pytest
|
||||
$ coverage html
|
||||
|
||||
Open ``htmlcov/index.html`` in your browser to explore the report.
|
||||
|
||||
Read more about `coverage <https://coverage.readthedocs.io>`__.
|
||||
|
||||
|
||||
Building the docs
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Build the docs in the ``docs`` directory using Sphinx.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd docs
|
||||
$ make html
|
||||
|
||||
Open ``_build/html/index.html`` in your browser to view the docs.
|
||||
|
||||
Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.
|
|
@ -0,0 +1,28 @@
|
|||
Copyright 2007 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,12 @@
|
|||
include CHANGES.rst
|
||||
include tox.ini
|
||||
include requirements/*.txt
|
||||
graft artwork
|
||||
graft docs
|
||||
prune docs/_build
|
||||
graft examples
|
||||
graft tests
|
||||
include src/werkzeug/py.typed
|
||||
include src/werkzeug/*.pyi
|
||||
graft src/werkzeug/debug/shared
|
||||
global-exclude *.pyc
|
|
@ -0,0 +1,91 @@
|
|||
Werkzeug
|
||||
========
|
||||
|
||||
*werkzeug* German noun: "tool". Etymology: *werk* ("work"), *zeug* ("stuff")
|
||||
|
||||
Werkzeug is a comprehensive `WSGI`_ web application library. It began as
|
||||
a simple collection of various utilities for WSGI applications and has
|
||||
become one of the most advanced WSGI utility libraries.
|
||||
|
||||
It includes:
|
||||
|
||||
- An interactive debugger that allows inspecting stack traces and
|
||||
source code in the browser with an interactive interpreter for any
|
||||
frame in the stack.
|
||||
- A full-featured request object with objects to interact with
|
||||
headers, query args, form data, files, and cookies.
|
||||
- A response object that can wrap other WSGI applications and handle
|
||||
streaming data.
|
||||
- A routing system for matching URLs to endpoints and generating URLs
|
||||
for endpoints, with an extensible system for capturing variables
|
||||
from URLs.
|
||||
- HTTP utilities to handle entity tags, cache control, dates, user
|
||||
agents, cookies, files, and more.
|
||||
- A threaded WSGI server for use while developing applications
|
||||
locally.
|
||||
- A test client for simulating HTTP requests during testing without
|
||||
requiring running a server.
|
||||
|
||||
Werkzeug doesn't enforce any dependencies. It is up to the developer to
|
||||
choose a template engine, database adapter, and even how to handle
|
||||
requests. It can be used to build all sorts of end user applications
|
||||
such as blogs, wikis, or bulletin boards.
|
||||
|
||||
`Flask`_ wraps Werkzeug, using it to handle the details of WSGI while
|
||||
providing more structure and patterns for defining powerful
|
||||
applications.
|
||||
|
||||
.. _WSGI: https://wsgi.readthedocs.io/en/latest/
|
||||
.. _Flask: https://www.palletsprojects.com/p/flask/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install and update using `pip`_:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pip install -U Werkzeug
|
||||
|
||||
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||
|
||||
|
||||
A Simple Example
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
@Request.application
|
||||
def application(request):
|
||||
return Response('Hello, World!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple('localhost', 4000, application)
|
||||
|
||||
|
||||
Donate
|
||||
------
|
||||
|
||||
The Pallets organization develops and supports Werkzeug and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
`please donate today`_.
|
||||
|
||||
.. _please donate today: https://palletsprojects.com/donate
|
||||
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
- Documentation: https://werkzeug.palletsprojects.com/
|
||||
- Changes: https://werkzeug.palletsprojects.com/changes/
|
||||
- PyPI Releases: https://pypi.org/project/Werkzeug/
|
||||
- Source Code: https://github.com/pallets/werkzeug/
|
||||
- Issue Tracker: https://github.com/pallets/werkzeug/issues/
|
||||
- Website: https://palletsprojects.com/p/werkzeug/
|
||||
- Twitter: https://twitter.com/PalletsTeam
|
||||
- Chat: https://discord.gg/pallets
|
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,19 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
After Width: | Height: | Size: 204 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,4 @@
|
|||
Changes
|
||||
=======
|
||||
|
||||
.. include:: ../CHANGES.rst
|
|
@ -0,0 +1,55 @@
|
|||
from pallets_sphinx_themes import get_version
|
||||
from pallets_sphinx_themes import ProjectLink
|
||||
|
||||
# Project --------------------------------------------------------------
|
||||
|
||||
project = "Werkzeug"
|
||||
copyright = "2007 Pallets"
|
||||
author = "Pallets"
|
||||
release, version = get_version("Werkzeug")
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
master_doc = "index"
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"pallets_sphinx_themes",
|
||||
"sphinx_issues",
|
||||
"sphinxcontrib.log_cabinet",
|
||||
]
|
||||
autoclass_content = "both"
|
||||
autodoc_typehints = "description"
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
||||
issues_github_path = "pallets/werkzeug"
|
||||
|
||||
# HTML -----------------------------------------------------------------
|
||||
|
||||
html_theme = "werkzeug"
|
||||
html_context = {
|
||||
"project_links": [
|
||||
ProjectLink("Donate", "https://palletsprojects.com/donate"),
|
||||
ProjectLink("PyPI Releases", "https://pypi.org/project/Werkzeug/"),
|
||||
ProjectLink("Source Code", "https://github.com/pallets/werkzeug/"),
|
||||
ProjectLink("Issue Tracker", "https://github.com/pallets/werkzeug/issues/"),
|
||||
ProjectLink("Website", "https://palletsprojects.com/p/werkzeug/"),
|
||||
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
|
||||
ProjectLink("Chat", "https://discord.gg/pallets"),
|
||||
]
|
||||
}
|
||||
html_sidebars = {
|
||||
"index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
|
||||
"**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
|
||||
}
|
||||
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
|
||||
html_static_path = ["_static"]
|
||||
html_favicon = "_static/favicon.ico"
|
||||
html_logo = "_static/werkzeug.png"
|
||||
html_title = f"Werkzeug Documentation ({version})"
|
||||
html_show_sourcelink = False
|
||||
|
||||
# LaTeX ----------------------------------------------------------------
|
||||
|
||||
latex_documents = [
|
||||
(master_doc, f"Werkzeug-{version}.tex", html_title, author, "manual")
|
||||
]
|
|
@ -0,0 +1,138 @@
|
|||
===============
|
||||
Data Structures
|
||||
===============
|
||||
|
||||
.. module:: werkzeug.datastructures
|
||||
|
||||
Werkzeug provides some subclasses of common Python objects to extend them
|
||||
with additional features. Some of them are used to make them immutable, others
|
||||
are used to change some semantics to better work with HTTP.
|
||||
|
||||
General Purpose
|
||||
===============
|
||||
|
||||
.. versionchanged:: 0.6
|
||||
The general purpose classes are now pickleable in each protocol as long
|
||||
as the contained objects are pickleable. This means that the
|
||||
:class:`FileMultiDict` won't be pickleable as soon as it contains a
|
||||
file.
|
||||
|
||||
.. autoclass:: TypeConversionDict
|
||||
:members:
|
||||
|
||||
.. autoclass:: ImmutableTypeConversionDict
|
||||
:members: copy
|
||||
|
||||
.. autoclass:: MultiDict
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: OrderedMultiDict
|
||||
|
||||
.. autoclass:: ImmutableMultiDict
|
||||
:members: copy
|
||||
|
||||
.. autoclass:: ImmutableOrderedMultiDict
|
||||
:members: copy
|
||||
|
||||
.. autoclass:: CombinedMultiDict
|
||||
|
||||
.. autoclass:: ImmutableDict
|
||||
:members: copy
|
||||
|
||||
.. autoclass:: ImmutableList
|
||||
|
||||
.. autoclass:: FileMultiDict
|
||||
:members:
|
||||
|
||||
.. _http-datastructures:
|
||||
|
||||
HTTP Related
|
||||
============
|
||||
|
||||
.. autoclass:: Headers([defaults])
|
||||
:members:
|
||||
|
||||
.. autoclass:: EnvironHeaders
|
||||
|
||||
.. autoclass:: HeaderSet
|
||||
:members:
|
||||
|
||||
.. autoclass:: Accept
|
||||
:members:
|
||||
|
||||
.. autoclass:: MIMEAccept
|
||||
:members: accept_html, accept_xhtml, accept_json
|
||||
|
||||
.. autoclass:: CharsetAccept
|
||||
|
||||
.. autoclass:: LanguageAccept
|
||||
|
||||
.. autoclass:: RequestCacheControl
|
||||
:members:
|
||||
|
||||
.. autoattribute:: no_cache
|
||||
|
||||
.. autoattribute:: no_store
|
||||
|
||||
.. autoattribute:: max_age
|
||||
|
||||
.. autoattribute:: no_transform
|
||||
|
||||
.. autoclass:: ResponseCacheControl
|
||||
:members:
|
||||
|
||||
.. autoattribute:: no_cache
|
||||
|
||||
.. autoattribute:: no_store
|
||||
|
||||
.. autoattribute:: max_age
|
||||
|
||||
.. autoattribute:: no_transform
|
||||
|
||||
.. autoclass:: ETags
|
||||
:members:
|
||||
|
||||
.. autoclass:: Authorization
|
||||
:members:
|
||||
|
||||
.. autoclass:: WWWAuthenticate
|
||||
:members:
|
||||
|
||||
.. autoclass:: IfRange
|
||||
:members:
|
||||
|
||||
.. autoclass:: Range
|
||||
:members:
|
||||
|
||||
.. autoclass:: ContentRange
|
||||
:members:
|
||||
|
||||
|
||||
Others
|
||||
======
|
||||
|
||||
.. autoclass:: FileStorage
|
||||
:members:
|
||||
|
||||
.. attribute:: stream
|
||||
|
||||
The input stream for the uploaded file. This usually points to an
|
||||
open temporary file.
|
||||
|
||||
.. attribute:: filename
|
||||
|
||||
The filename of the file on the client. Can be a ``str``, or an
|
||||
instance of ``os.PathLike``.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The name of the form field.
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
The multipart headers as :class:`Headers` object. This usually contains
|
||||
irrelevant information but in combination with custom multipart requests
|
||||
the raw headers might be interesting.
|
||||
|
||||
.. versionadded:: 0.6
|
|
@ -0,0 +1,101 @@
|
|||
Debugging Applications
|
||||
======================
|
||||
|
||||
.. module:: werkzeug.debug
|
||||
|
||||
Depending on the WSGI gateway/server, exceptions are handled
|
||||
differently. Most of the time, exceptions go to stderr or the error log,
|
||||
and a generic "500 Internal Server Error" message is displayed.
|
||||
|
||||
Since this is not the best debugging environment, Werkzeug provides a
|
||||
WSGI middleware that renders nice tracebacks, optionally with an
|
||||
interactive debug console to execute code in any frame.
|
||||
|
||||
.. danger::
|
||||
|
||||
The debugger allows the execution of arbitrary code which makes it a
|
||||
major security risk. **The debugger must never be used on production
|
||||
machines. We cannot stress this enough. Do not enable the debugger
|
||||
in production.**
|
||||
|
||||
.. note::
|
||||
|
||||
The interactive debugger does not work in forking environments, such
|
||||
as a server that starts multiple processes. Most such environments
|
||||
are production servers, where the debugger should not be enabled
|
||||
anyway.
|
||||
|
||||
|
||||
Enabling the Debugger
|
||||
---------------------
|
||||
|
||||
Enable the debugger by wrapping the application with the
|
||||
:class:`DebuggedApplication` middleware. Alternatively, you can pass
|
||||
``use_debugger=True`` to :func:`run_simple` and it will do that for you.
|
||||
|
||||
.. autoclass:: DebuggedApplication
|
||||
|
||||
|
||||
Using the Debugger
|
||||
------------------
|
||||
|
||||
Once enabled and an error happens during a request you will see a
|
||||
detailed traceback instead of a generic "internal server error". The
|
||||
traceback is still output to the terminal as well.
|
||||
|
||||
The error message is displayed at the top. Clicking it jumps to the
|
||||
bottom of the traceback. Frames that represent user code, as opposed to
|
||||
built-ins or installed packages, are highlighted blue. Clicking a
|
||||
frame will show more lines for context, clicking again will hide them.
|
||||
|
||||
If you have the ``evalex`` feature enabled you can get a console for
|
||||
every frame in the traceback by hovering over a frame and clicking the
|
||||
console icon that appears at the right. Once clicked a console opens
|
||||
where you can execute Python code in:
|
||||
|
||||
.. image:: _static/debug-screenshot.png
|
||||
:alt: a screenshot of the interactive debugger
|
||||
:align: center
|
||||
|
||||
Inside the interactive consoles you can execute any kind of Python code.
|
||||
Unlike regular Python consoles the output of the object reprs is colored
|
||||
and stripped to a reasonable size by default. If the output is longer
|
||||
than what the console decides to display a small plus sign is added to
|
||||
the repr and a click will expand the repr.
|
||||
|
||||
To display all variables that are defined in the current frame you can
|
||||
use the ``dump()`` function. You can call it without arguments to get a
|
||||
detailed list of all variables and their values, or with an object as
|
||||
argument to get a detailed list of all the attributes it has.
|
||||
|
||||
|
||||
Debugger PIN
|
||||
------------
|
||||
|
||||
Starting with Werkzeug 0.11 the debug console is protected by a PIN.
|
||||
This is a security helper to make it less likely for the debugger to be
|
||||
exploited if you forget to disable it when deploying to production. The
|
||||
PIN based authentication is enabled by default.
|
||||
|
||||
The first time a console is opened, a dialog will prompt for a PIN that
|
||||
is printed to the command line. The PIN is generated in a stable way
|
||||
that is specific to the project. An explicit PIN can be provided through
|
||||
the environment variable ``WERKZEUG_DEBUG_PIN``. This can be set to a
|
||||
number and will become the PIN. This variable can also be set to the
|
||||
value ``off`` to disable the PIN check entirely.
|
||||
|
||||
If an incorrect PIN is entered too many times the server needs to be
|
||||
restarted.
|
||||
|
||||
**This feature is not meant to entirely secure the debugger. It is
|
||||
intended to make it harder for an attacker to exploit the debugger.
|
||||
Never enable the debugger in production.**
|
||||
|
||||
|
||||
Pasting Errors
|
||||
--------------
|
||||
|
||||
If you click on the "Traceback (most recent call last)" header, the
|
||||
view switches to a traditional text-based traceback. You can copy and
|
||||
paste this in order to provide information when asking a question or
|
||||
reporting an issue.
|
|
@ -0,0 +1,82 @@
|
|||
Apache httpd
|
||||
============
|
||||
|
||||
`Apache httpd`_ is a fast, production level HTTP server. When serving
|
||||
your application with one of the WSGI servers listed in :doc:`index`, it
|
||||
is often good or necessary to put a dedicated HTTP server in front of
|
||||
it. This "reverse proxy" can handle incoming requests, TLS, and other
|
||||
security and performance concerns better than the WSGI server.
|
||||
|
||||
httpd can be installed using your system package manager, or a pre-built
|
||||
executable for Windows. Installing and running httpd itself is outside
|
||||
the scope of this doc. This page outlines the basics of configuring
|
||||
httpd to proxy your application. Be sure to read its documentation to
|
||||
understand what features are available.
|
||||
|
||||
.. _Apache httpd: https://httpd.apache.org/
|
||||
|
||||
|
||||
Domain Name
|
||||
-----------
|
||||
|
||||
Acquiring and configuring a domain name is outside the scope of this
|
||||
doc. In general, you will buy a domain name from a registrar, pay for
|
||||
server space with a hosting provider, and then point your registrar
|
||||
at the hosting provider's name servers.
|
||||
|
||||
To simulate this, you can also edit your ``hosts`` file, located at
|
||||
``/etc/hosts`` on Linux. Add a line that associates a name with the
|
||||
local IP.
|
||||
|
||||
Modern Linux systems may be configured to treat any domain name that
|
||||
ends with ``.localhost`` like this without adding it to the ``hosts``
|
||||
file.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``/etc/hosts``
|
||||
|
||||
127.0.0.1 hello.localhost
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on
|
||||
Linux. It may be different depending on your operating system. Check the
|
||||
docs and look for ``httpd.conf``.
|
||||
|
||||
Remove or comment out any existing ``DocumentRoot`` directive. Add the
|
||||
config lines below. We'll assume the WSGI server is listening locally at
|
||||
``http://127.0.0.1:8000``.
|
||||
|
||||
.. code-block:: apache
|
||||
:caption: ``/etc/httpd/conf/httpd.conf``
|
||||
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||
ProxyPass / http://127.0.0.1:8000/
|
||||
RequestHeader set X-Forwarded-Proto http
|
||||
RequestHeader set X-Forwarded-Prefix /
|
||||
|
||||
The ``LoadModule`` lines might already exist. If so, make sure they are
|
||||
uncommented instead of adding them manually.
|
||||
|
||||
Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded``
|
||||
headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically
|
||||
set by ``ProxyPass``.
|
||||
|
||||
|
||||
Static Files
|
||||
------------
|
||||
|
||||
If your application has static files such as JavaScript, CSS, and
|
||||
images, it will be more efficient to let Nginx serve them directly
|
||||
rather than going through the Python application.
|
||||
|
||||
Assuming the static files are expected to be available under the
|
||||
``/static/`` URL, and are stored at ``/home/project/static/``, add the
|
||||
following to the config above.
|
||||
|
||||
.. code-block:: apache
|
||||
|
||||
Alias /static/ /home/project/static/
|
|
@ -0,0 +1,80 @@
|
|||
eventlet
|
||||
========
|
||||
|
||||
Prefer using :doc:`gunicorn` with eventlet workers rather than using
|
||||
`eventlet`_ directly. Gunicorn provides a much more configurable and
|
||||
production-tested server.
|
||||
|
||||
`eventlet`_ allows writing asynchronous, coroutine-based code that looks
|
||||
like standard synchronous Python. It uses `greenlet`_ to enable task
|
||||
switching without writing ``async/await`` or using ``asyncio``.
|
||||
|
||||
:doc:`gevent` is another library that does the same thing. Certain
|
||||
dependencies you have, or other considerations, may affect which of the
|
||||
two you choose to use.
|
||||
|
||||
eventlet provides a WSGI server that can handle many connections at once
|
||||
instead of one per worker process. You must actually use eventlet in
|
||||
your own code to see any benefit to using the server.
|
||||
|
||||
.. _eventlet: https://eventlet.net/
|
||||
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
When using eventlet, greenlet>=1.0 is required, otherwise context locals
|
||||
such as ``request`` will not work as expected. When using PyPy,
|
||||
PyPy>=7.3.7 is required.
|
||||
|
||||
Create a virtualenv, install your application, then install
|
||||
``eventlet``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install eventlet
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
To use eventlet to serve your application, write a script that imports
|
||||
its ``wsgi.server``, as well as your app or app factory.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``wsgi.py``
|
||||
|
||||
import eventlet
|
||||
from eventlet import wsgi
|
||||
from hello import create_app
|
||||
|
||||
app = create_app()
|
||||
wsgi.server(eventlet.listen(("127.0.0.1", 8000), app)
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python wsgi.py
|
||||
(x) wsgi starting up on http://127.0.0.1:8000
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
eventlet should not be run as root because it would cause your
|
||||
application code to run as root, which is not secure. However, this
|
||||
means it will not be possible to bind to port 80 or 443. Instead, a
|
||||
reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used
|
||||
in front of eventlet.
|
||||
|
||||
You can bind to all external IPs on a non-privileged port by using
|
||||
``0.0.0.0`` in the server arguments shown in the previous section.
|
||||
Don't do this when using a reverse proxy setup, otherwise it will be
|
||||
possible to bypass the proxy.
|
||||
|
||||
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
|
||||
IP address in your browser.
|
|
@ -0,0 +1,80 @@
|
|||
gevent
|
||||
======
|
||||
|
||||
Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather
|
||||
than using `gevent`_ directly. Gunicorn and uWSGI provide much more
|
||||
configurable and production-tested servers.
|
||||
|
||||
`gevent`_ allows writing asynchronous, coroutine-based code that looks
|
||||
like standard synchronous Python. It uses `greenlet`_ to enable task
|
||||
switching without writing ``async/await`` or using ``asyncio``.
|
||||
|
||||
:doc:`eventlet` is another library that does the same thing. Certain
|
||||
dependencies you have, or other considerations, may affect which of the
|
||||
two you choose to use.
|
||||
|
||||
gevent provides a WSGI server that can handle many connections at once
|
||||
instead of one per worker process. You must actually use gevent in your
|
||||
own code to see any benefit to using the server.
|
||||
|
||||
.. _gevent: https://www.gevent.org/
|
||||
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
When using gevent, greenlet>=1.0 is required, otherwise context locals
|
||||
such as ``request`` will not work as expected. When using PyPy,
|
||||
PyPy>=7.3.7 is required.
|
||||
|
||||
Create a virtualenv, install your application, then install ``gevent``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install gevent
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
To use gevent to serve your application, write a script that imports its
|
||||
``WSGIServer``, as well as your app or app factory.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``wsgi.py``
|
||||
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from hello import create_app
|
||||
|
||||
app = create_app()
|
||||
http_server = WSGIServer(("127.0.0.1", 8000), app)
|
||||
http_server.serve_forever()
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python wsgi.py
|
||||
|
||||
No output is shown when the server starts.
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
gevent should not be run as root because it would cause your
|
||||
application code to run as root, which is not secure. However, this
|
||||
means it will not be possible to bind to port 80 or 443. Instead, a
|
||||
reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used
|
||||
in front of gevent.
|
||||
|
||||
You can bind to all external IPs on a non-privileged port by using
|
||||
``0.0.0.0`` in the server arguments shown in the previous section. Don't
|
||||
do this when using a reverse proxy setup, otherwise it will be possible
|
||||
to bypass the proxy.
|
||||
|
||||
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
|
||||
IP address in your browser.
|
|
@ -0,0 +1,130 @@
|
|||
Gunicorn
|
||||
========
|
||||
|
||||
`Gunicorn`_ is a pure Python WSGI server with simple configuration and
|
||||
multiple worker implementations for performance tuning.
|
||||
|
||||
* It tends to integrate easily with hosting platforms.
|
||||
* It does not support Windows (but does run on WSL).
|
||||
* It is easy to install as it does not require additional dependencies
|
||||
or compilation.
|
||||
* It has built-in async worker support using gevent or eventlet.
|
||||
|
||||
This page outlines the basics of running Gunicorn. Be sure to read its
|
||||
`documentation`_ and use ``gunicorn --help`` to understand what features
|
||||
are available.
|
||||
|
||||
.. _Gunicorn: https://gunicorn.org/
|
||||
.. _documentation: https://docs.gunicorn.org/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Gunicorn is easy to install, as it does not require external
|
||||
dependencies or compilation. It runs on Windows only under WSL.
|
||||
|
||||
Create a virtualenv, install your application, then install
|
||||
``gunicorn``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install gunicorn
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The only required argument to Gunicorn tells it how to load your
|
||||
application. The syntax is ``{module_import}:{app_variable}``.
|
||||
``module_import`` is the dotted import name to the module with your
|
||||
application. ``app_variable`` is the variable with the application. It
|
||||
can also be a function call (with any arguments) if you're using the
|
||||
app factory pattern.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# equivalent to 'from hello import app'
|
||||
$ gunicorn -w 4 'hello:app'
|
||||
|
||||
# equivalent to 'from hello import create_app; create_app()'
|
||||
$ gunicorn -w 4 'hello:create_app()'
|
||||
|
||||
Starting gunicorn 20.1.0
|
||||
Listening at: http://127.0.0.1:8000 (x)
|
||||
Using worker: sync
|
||||
Booting worker with pid: x
|
||||
Booting worker with pid: x
|
||||
Booting worker with pid: x
|
||||
Booting worker with pid: x
|
||||
|
||||
The ``-w`` option specifies the number of processes to run; a starting
|
||||
value could be ``CPU * 2``. The default is only 1 worker, which is
|
||||
probably not what you want for the default worker type.
|
||||
|
||||
Logs for each request aren't shown by default, only worker info and
|
||||
errors are shown. To show access logs on stdout, use the
|
||||
``--access-logfile=-`` option.
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
Gunicorn should not be run as root because it would cause your
|
||||
application code to run as root, which is not secure. However, this
|
||||
means it will not be possible to bind to port 80 or 443. Instead, a
|
||||
reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used
|
||||
in front of Gunicorn.
|
||||
|
||||
You can bind to all external IPs on a non-privileged port using the
|
||||
``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup,
|
||||
otherwise it will be possible to bypass the proxy.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()'
|
||||
Listening at: http://0.0.0.0:8000 (x)
|
||||
|
||||
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
|
||||
IP address in your browser.
|
||||
|
||||
|
||||
Async with gevent or eventlet
|
||||
-----------------------------
|
||||
|
||||
The default sync worker is appropriate for many use cases. If you need
|
||||
asynchronous support, Gunicorn provides workers using either `gevent`_
|
||||
or `eventlet`_. This is not the same as Python's ``async/await``, or the
|
||||
ASGI server spec. You must actually use gevent/eventlet in your own code
|
||||
to see any benefit to using the workers.
|
||||
|
||||
When using either gevent or eventlet, greenlet>=1.0 is required,
|
||||
otherwise context locals such as ``request`` will not work as expected.
|
||||
When using PyPy, PyPy>=7.3.7 is required.
|
||||
|
||||
To use gevent:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ gunicorn -k gevent 'hello:create_app()'
|
||||
Starting gunicorn 20.1.0
|
||||
Listening at: http://127.0.0.1:8000 (x)
|
||||
Using worker: gevent
|
||||
Booting worker with pid: x
|
||||
|
||||
To use eventlet:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ gunicorn -k eventlet 'hello:create_app()'
|
||||
Starting gunicorn 20.1.0
|
||||
Listening at: http://127.0.0.1:8000 (x)
|
||||
Using worker: eventlet
|
||||
Booting worker with pid: x
|
||||
|
||||
.. _gevent: https://www.gevent.org/
|
||||
.. _eventlet: https://eventlet.net/
|
|
@ -0,0 +1,71 @@
|
|||
Deploying to Production
|
||||
=======================
|
||||
|
||||
After developing your application, you'll want to make it available
|
||||
publicly to other users. When you're developing locally, you're probably
|
||||
using the built-in development server, debugger, and reloader. These
|
||||
should not be used in production. Instead, you should use a dedicated
|
||||
WSGI server or hosting platform, some of which will be described here.
|
||||
|
||||
"Production" means "not development", which applies whether you're
|
||||
serving your application publicly to millions of users or privately /
|
||||
locally to a single user. **Do not use the development server when
|
||||
deploying to production. It is intended for use only during local
|
||||
development. It is not designed to be particularly secure, stable, or
|
||||
efficient.**
|
||||
|
||||
Self-Hosted Options
|
||||
-------------------
|
||||
|
||||
Werkzeug is a WSGI *application*. A WSGI *server* is used to run the
|
||||
application, converting incoming HTTP requests to the standard WSGI
|
||||
environ, and converting outgoing WSGI responses to HTTP responses.
|
||||
|
||||
The primary goal of these docs is to familiarize you with the concepts
|
||||
involved in running a WSGI application using a production WSGI server
|
||||
and HTTP server. There are many WSGI servers and HTTP servers, with many
|
||||
configuration possibilities. The pages below discuss the most common
|
||||
servers, and show the basics of running each one. The next section
|
||||
discusses platforms that can manage this for you.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
gunicorn
|
||||
waitress
|
||||
mod_wsgi
|
||||
uwsgi
|
||||
gevent
|
||||
eventlet
|
||||
|
||||
WSGI servers have HTTP servers built-in. However, a dedicated HTTP
|
||||
server may be safer, more efficient, or more capable. Putting an HTTP
|
||||
server in front of the WSGI server is called a "reverse proxy."
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
proxy_fix
|
||||
nginx
|
||||
apache-httpd
|
||||
|
||||
This list is not exhaustive, and you should evaluate these and other
|
||||
servers based on your application's needs. Different servers will have
|
||||
different capabilities, configuration, and support.
|
||||
|
||||
|
||||
Hosting Platforms
|
||||
-----------------
|
||||
|
||||
There are many services available for hosting web applications without
|
||||
needing to maintain your own server, networking, domain, etc. Some
|
||||
services may have a free tier up to a certain time or bandwidth. Many of
|
||||
these services use one of the WSGI servers described above, or a similar
|
||||
interface.
|
||||
|
||||
You should evaluate services based on your application's needs.
|
||||
Different services will have different capabilities, configuration,
|
||||
pricing, and support.
|
||||
|
||||
You'll probably need to :doc:`proxy_fix` when using most hosting
|
||||
platforms.
|
|
@ -0,0 +1,94 @@
|
|||
mod_wsgi
|
||||
========
|
||||
|
||||
`mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server.
|
||||
The modern `mod_wsgi-express`_ command makes it easy to configure and
|
||||
start the server without needing to write Apache httpd configuration.
|
||||
|
||||
* Tightly integrated with Apache httpd.
|
||||
* Supports Windows directly.
|
||||
* Requires a compiler and the Apache development headers to install.
|
||||
* Does not require a reverse proxy setup.
|
||||
|
||||
This page outlines the basics of running mod_wsgi-express, not the more
|
||||
complex installation and configuration with httpd. Be sure to read the
|
||||
`mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to
|
||||
understand what features are available.
|
||||
|
||||
.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/
|
||||
.. _mod_wsgi: https://modwsgi.readthedocs.io/
|
||||
.. _Apache httpd: https://httpd.apache.org/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Installing mod_wsgi requires a compiler and the Apache server and
|
||||
development headers installed. You will get an error if they are not.
|
||||
How to install them depends on the OS and package manager that you use.
|
||||
|
||||
Create a virtualenv, install your application, then install
|
||||
``mod_wsgi``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install mod_wsgi
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The only argument to ``mod_wsgi-express`` specifies a script containing
|
||||
your application, which must be called ``application``. You can
|
||||
write a small script to import your app with this name, or to create it
|
||||
if using the app factory pattern.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``wsgi.py``
|
||||
|
||||
from hello import app
|
||||
|
||||
application = app
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``wsgi.py``
|
||||
|
||||
from hello import create_app
|
||||
|
||||
application = create_app()
|
||||
|
||||
Now run the ``mod_wsgi-express start-server`` command.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ mod_wsgi-express start-server wsgi.py --processes 4
|
||||
|
||||
The ``--processes`` option specifies the number of worker processes to
|
||||
run; a starting value could be ``CPU * 2``.
|
||||
|
||||
Logs for each request aren't show in the terminal. If an error occurs,
|
||||
its information is written to the error log file shown when starting the
|
||||
server.
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
Unlike the other WSGI servers in these docs, mod_wsgi can be run as
|
||||
root to bind to privileged ports like 80 and 443. However, it must be
|
||||
configured to drop permissions to a different user and group for the
|
||||
worker processes.
|
||||
|
||||
For example, if you created a ``hello`` user and group, you should
|
||||
install your virtualenv and application as that user, then tell
|
||||
mod_wsgi to drop to that user after starting.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ sudo /home/hello/venv/bin/mod_wsgi-express start-server \
|
||||
/home/hello/wsgi.py \
|
||||
--user hello --group hello --port 80 --processes 4
|
|
@ -0,0 +1,87 @@
|
|||
nginx
|
||||
=====
|
||||
|
||||
`nginx`_ is a fast, production level HTTP server. When serving your
|
||||
application with one of the WSGI servers listed in :doc:`index`, it is
|
||||
often good or necessary to put a dedicated HTTP server in front of it.
|
||||
This "reverse proxy" can handle incoming requests, TLS, and other
|
||||
security and performance concerns better than the WSGI server.
|
||||
|
||||
Nginx can be installed using your system package manager, or a pre-built
|
||||
executable for Windows. Installing and running Nginx itself is outside
|
||||
the scope of this doc. This page outlines the basics of configuring
|
||||
Nginx to proxy your application. Be sure to read its documentation to
|
||||
understand what features are available.
|
||||
|
||||
.. _nginx: https://nginx.org/
|
||||
|
||||
|
||||
Domain Name
|
||||
-----------
|
||||
|
||||
Acquiring and configuring a domain name is outside the scope of this
|
||||
doc. In general, you will buy a domain name from a registrar, pay for
|
||||
server space with a hosting provider, and then point your registrar
|
||||
at the hosting provider's name servers.
|
||||
|
||||
To simulate this, you can also edit your ``hosts`` file, located at
|
||||
``/etc/hosts`` on Linux. Add a line that associates a name with the
|
||||
local IP.
|
||||
|
||||
Modern Linux systems may be configured to treat any domain name that
|
||||
ends with ``.localhost`` like this without adding it to the ``hosts``
|
||||
file.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``/etc/hosts``
|
||||
|
||||
127.0.0.1 hello.localhost
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The nginx configuration is located at ``/etc/nginx/nginx.conf`` on
|
||||
Linux. It may be different depending on your operating system. Check the
|
||||
docs and look for ``nginx.conf``.
|
||||
|
||||
Remove or comment out any existing ``server`` section. Add a ``server``
|
||||
section and use the ``proxy_pass`` directive to point to the address the
|
||||
WSGI server is listening on. We'll assume the WSGI server is listening
|
||||
locally at ``http://127.0.0.1:8000``.
|
||||
|
||||
.. code-block:: nginx
|
||||
:caption: ``/etc/nginx.conf``
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000/;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /;
|
||||
}
|
||||
}
|
||||
|
||||
Then :doc:`proxy_fix` so that your application uses these headers.
|
||||
|
||||
|
||||
Static Files
|
||||
------------
|
||||
|
||||
If your application has static files such as JavaScript, CSS, and
|
||||
images, it will be more efficient to let Nginx serve them directly
|
||||
rather than going through the Python application.
|
||||
|
||||
Assuming the static files are expected to be available under the
|
||||
``/static/`` URL, and are stored at ``/home/project/static/``, add the
|
||||
following to the ``server`` block above.
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
location /static {
|
||||
alias /home/project/static;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
Tell Werkzeug it is Behind a Proxy
|
||||
==================================
|
||||
|
||||
When using a reverse proxy, or many Python hosting platforms, the proxy
|
||||
will intercept and forward all external requests to the local WSGI
|
||||
server.
|
||||
|
||||
From the WSGI server and application's perspectives, requests are now
|
||||
coming from the HTTP server to the local address, rather than from
|
||||
the remote address to the external server address.
|
||||
|
||||
HTTP servers should set ``X-Forwarded-`` headers to pass on the real
|
||||
values to the application. The application can then be told to trust and
|
||||
use those values by wrapping it with the
|
||||
:doc:`../middleware/proxy_fix` middleware provided by Werkzeug.
|
||||
|
||||
This middleware should only be used if the application is actually
|
||||
behind a proxy, and should be configured with the number of proxies that
|
||||
are chained in front of it. Not all proxies set all the headers. Since
|
||||
incoming headers can be faked, you must set how many proxies are setting
|
||||
each header so the middleware knows what to trust.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
|
||||
)
|
||||
|
||||
Remember, only apply this middleware if you are behind a proxy, and set
|
||||
the correct number of proxies that set each header. It can be a security
|
||||
issue if you get this configuration wrong.
|
|
@ -0,0 +1,145 @@
|
|||
uWSGI
|
||||
=====
|
||||
|
||||
`uWSGI`_ is a fast, compiled server suite with extensive configuration
|
||||
and capabilities beyond a basic server.
|
||||
|
||||
* It can be very performant due to being a compiled program.
|
||||
* It is complex to configure beyond the basic application, and has so
|
||||
many options that it can be difficult for beginners to understand.
|
||||
* It does not support Windows (but does run on WSL).
|
||||
* It requires a compiler to install in some cases.
|
||||
|
||||
This page outlines the basics of running uWSGI. Be sure to read its
|
||||
documentation to understand what features are available.
|
||||
|
||||
.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
uWSGI has multiple ways to install it. The most straightforward is to
|
||||
install the ``pyuwsgi`` package, which provides precompiled wheels for
|
||||
common platforms. However, it does not provide SSL support, which can be
|
||||
provided with a reverse proxy instead.
|
||||
|
||||
Create a virtualenv, install your application, then install ``pyuwsgi``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install pyuwsgi
|
||||
|
||||
If you have a compiler available, you can install the ``uwsgi`` package
|
||||
instead. Or install the ``pyuwsgi`` package from sdist instead of wheel.
|
||||
Either method will include SSL support.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install uwsgi
|
||||
|
||||
# or
|
||||
$ pip install --no-binary pyuwsgi pyuwsgi
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The most basic way to run uWSGI is to tell it to start an HTTP server
|
||||
and import your application.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app
|
||||
|
||||
*** Starting uWSGI 2.0.20 (64bit) on [x] ***
|
||||
*** Operational MODE: preforking ***
|
||||
mounting hello:app on /
|
||||
spawned uWSGI master process (pid: x)
|
||||
spawned uWSGI worker 1 (pid: x, cores: 1)
|
||||
spawned uWSGI worker 2 (pid: x, cores: 1)
|
||||
spawned uWSGI worker 3 (pid: x, cores: 1)
|
||||
spawned uWSGI worker 4 (pid: x, cores: 1)
|
||||
spawned uWSGI http 1 (pid: x)
|
||||
|
||||
If you're using the app factory pattern, you'll need to create a small
|
||||
Python file to create the app, then point uWSGI at that.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``wsgi.py``
|
||||
|
||||
from hello import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app
|
||||
|
||||
The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The
|
||||
``--master`` option specifies the standard worker manager. The ``-p``
|
||||
option starts 4 worker processes; a starting value could be ``CPU * 2``.
|
||||
The ``-w`` option tells uWSGI how to import your application
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
uWSGI should not be run as root with the configuration shown in this doc
|
||||
because it would cause your application code to run as root, which is
|
||||
not secure. However, this means it will not be possible to bind to port
|
||||
80 or 443. Instead, a reverse proxy such as :doc:`nginx` or
|
||||
:doc:`apache-httpd` should be used in front of uWSGI. It is possible to
|
||||
run uWSGI as root securely, but that is beyond the scope of this doc.
|
||||
|
||||
uWSGI has optimized integration with `Nginx uWSGI`_ and
|
||||
`Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using
|
||||
a standard HTTP proxy. That configuration is beyond the scope of this
|
||||
doc, see the links for more information.
|
||||
|
||||
.. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html
|
||||
.. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi
|
||||
|
||||
You can bind to all external IPs on a non-privileged port using the
|
||||
``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy
|
||||
setup, otherwise it will be possible to bypass the proxy.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app
|
||||
|
||||
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
|
||||
IP address in your browser.
|
||||
|
||||
|
||||
Async with gevent
|
||||
-----------------
|
||||
|
||||
The default sync worker is appropriate for many use cases. If you need
|
||||
asynchronous support, uWSGI provides a `gevent`_ worker. This is not the
|
||||
same as Python's ``async/await``, or the ASGI server spec. You must
|
||||
actually use gevent in your own code to see any benefit to using the
|
||||
worker.
|
||||
|
||||
When using gevent, greenlet>=1.0 is required, otherwise context locals
|
||||
such as ``request`` will not work as expected. When using PyPy,
|
||||
PyPy>=7.3.7 is required.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app
|
||||
|
||||
*** Starting uWSGI 2.0.20 (64bit) on [x] ***
|
||||
*** Operational MODE: async ***
|
||||
mounting hello:app on /
|
||||
spawned uWSGI master process (pid: x)
|
||||
spawned uWSGI worker 1 (pid: x, cores: 100)
|
||||
spawned uWSGI http 1 (pid: x)
|
||||
*** running gevent loop engine [addr:x] ***
|
||||
|
||||
|
||||
.. _gevent: https://www.gevent.org/
|
|
@ -0,0 +1,75 @@
|
|||
Waitress
|
||||
========
|
||||
|
||||
`Waitress`_ is a pure Python WSGI server.
|
||||
|
||||
* It is easy to configure.
|
||||
* It supports Windows directly.
|
||||
* It is easy to install as it does not require additional dependencies
|
||||
or compilation.
|
||||
* It does not support streaming requests, full request data is always
|
||||
buffered.
|
||||
* It uses a single process with multiple thread workers.
|
||||
|
||||
This page outlines the basics of running Waitress. Be sure to read its
|
||||
documentation and ``waitress-serve --help`` to understand what features
|
||||
are available.
|
||||
|
||||
.. _Waitress: https://docs.pylonsproject.org/projects/waitress/
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Create a virtualenv, install your application, then install
|
||||
``waitress``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd hello-app
|
||||
$ python -m venv venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install . # install your application
|
||||
$ pip install waitress
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The only required argument to ``waitress-serve`` tells it how to load
|
||||
your application. The syntax is ``{module}:{app}``. ``module`` is
|
||||
the dotted import name to the module with your application. ``app`` is
|
||||
the variable with the application. If you're using the app factory
|
||||
pattern, use ``--call {module}:{factory}`` instead.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
# equivalent to 'from hello import app'
|
||||
$ waitress-serve hello:app --host 127.0.0.1
|
||||
|
||||
# equivalent to 'from hello import create_app; create_app()'
|
||||
$ waitress-serve --call hello:create_app --host 127.0.0.1
|
||||
|
||||
Serving on http://127.0.0.1:8080
|
||||
|
||||
The ``--host`` option binds the server to local ``127.0.0.1`` only.
|
||||
|
||||
Logs for each request aren't shown, only errors are shown. Logging can
|
||||
be configured through the Python interface instead of the command line.
|
||||
|
||||
|
||||
Binding Externally
|
||||
------------------
|
||||
|
||||
Waitress should not be run as root because it would cause your
|
||||
application code to run as root, which is not secure. However, this
|
||||
means it will not be possible to bind to port 80 or 443. Instead, a
|
||||
reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used
|
||||
in front of Waitress.
|
||||
|
||||
You can bind to all external IPs on a non-privileged port by not
|
||||
specifying the ``--host`` option. Don't do this when using a revers
|
||||
proxy setup, otherwise it will be possible to bypass the proxy.
|
||||
|
||||
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
|
||||
IP address in your browser.
|
|
@ -0,0 +1,155 @@
|
|||
===============
|
||||
HTTP Exceptions
|
||||
===============
|
||||
|
||||
.. automodule:: werkzeug.exceptions
|
||||
|
||||
|
||||
Error Classes
|
||||
=============
|
||||
|
||||
The following error classes exist in Werkzeug:
|
||||
|
||||
.. autoexception:: BadRequest
|
||||
|
||||
.. autoexception:: Unauthorized
|
||||
|
||||
.. autoexception:: Forbidden
|
||||
|
||||
.. autoexception:: NotFound
|
||||
|
||||
.. autoexception:: MethodNotAllowed
|
||||
|
||||
.. autoexception:: NotAcceptable
|
||||
|
||||
.. autoexception:: RequestTimeout
|
||||
|
||||
.. autoexception:: Conflict
|
||||
|
||||
.. autoexception:: Gone
|
||||
|
||||
.. autoexception:: LengthRequired
|
||||
|
||||
.. autoexception:: PreconditionFailed
|
||||
|
||||
.. autoexception:: RequestEntityTooLarge
|
||||
|
||||
.. autoexception:: RequestURITooLarge
|
||||
|
||||
.. autoexception:: UnsupportedMediaType
|
||||
|
||||
.. autoexception:: RequestedRangeNotSatisfiable
|
||||
|
||||
.. autoexception:: ExpectationFailed
|
||||
|
||||
.. autoexception:: ImATeapot
|
||||
|
||||
.. autoexception:: UnprocessableEntity
|
||||
|
||||
.. autoexception:: Locked
|
||||
|
||||
.. autoexception:: FailedDependency
|
||||
|
||||
.. autoexception:: PreconditionRequired
|
||||
|
||||
.. autoexception:: TooManyRequests
|
||||
|
||||
.. autoexception:: RequestHeaderFieldsTooLarge
|
||||
|
||||
.. autoexception:: UnavailableForLegalReasons
|
||||
|
||||
.. autoexception:: InternalServerError
|
||||
:members:
|
||||
|
||||
.. autoexception:: NotImplemented
|
||||
|
||||
.. autoexception:: BadGateway
|
||||
|
||||
.. autoexception:: ServiceUnavailable
|
||||
|
||||
.. autoexception:: GatewayTimeout
|
||||
|
||||
.. autoexception:: HTTPVersionNotSupported
|
||||
|
||||
.. autoexception:: ClientDisconnected
|
||||
|
||||
.. autoexception:: SecurityError
|
||||
|
||||
|
||||
Baseclass
|
||||
=========
|
||||
|
||||
All the exceptions implement this common interface:
|
||||
|
||||
.. autoexception:: HTTPException
|
||||
:members: get_response, __call__
|
||||
|
||||
|
||||
Special HTTP Exceptions
|
||||
=======================
|
||||
|
||||
Starting with Werkzeug 0.3 some of the builtin classes raise exceptions that
|
||||
look like regular python exceptions (eg :exc:`KeyError`) but are
|
||||
:exc:`BadRequest` HTTP exceptions at the same time. This decision was made
|
||||
to simplify a common pattern where you want to abort if the client tampered
|
||||
with the submitted form data in a way that the application can't recover
|
||||
properly and should abort with ``400 BAD REQUEST``.
|
||||
|
||||
Assuming the application catches all HTTP exceptions and reacts to them
|
||||
properly a view function could do the following safely and doesn't have to
|
||||
check if the keys exist::
|
||||
|
||||
def new_post(request):
|
||||
post = Post(title=request.form['title'], body=request.form['body'])
|
||||
post.save()
|
||||
return redirect(post.url)
|
||||
|
||||
If `title` or `body` are missing in the form, a special key error will be
|
||||
raised which behaves like a :exc:`KeyError` but also a :exc:`BadRequest`
|
||||
exception.
|
||||
|
||||
.. autoexception:: BadRequestKeyError
|
||||
|
||||
|
||||
Simple Aborting
|
||||
===============
|
||||
|
||||
Sometimes it's convenient to just raise an exception by the error code,
|
||||
without importing the exception and looking up the name etc. For this
|
||||
purpose there is the :func:`abort` function.
|
||||
|
||||
.. autofunction:: abort
|
||||
|
||||
If you want to use this functionality with custom exceptions you can
|
||||
create an instance of the aborter class:
|
||||
|
||||
.. autoclass:: Aborter
|
||||
|
||||
|
||||
Custom Errors
|
||||
=============
|
||||
|
||||
As you can see from the list above not all status codes are available as
|
||||
errors. Especially redirects and other non 200 status codes that do not
|
||||
represent errors are missing. For redirects you can use the :func:`redirect`
|
||||
function from the utilities.
|
||||
|
||||
If you want to add an error yourself you can subclass :exc:`HTTPException`::
|
||||
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
class PaymentRequired(HTTPException):
|
||||
code = 402
|
||||
description = '<p>Payment required.</p>'
|
||||
|
||||
This is the minimal code you need for your own exception. If you want to
|
||||
add more logic to the errors you can override the
|
||||
:meth:`~HTTPException.get_description`, :meth:`~HTTPException.get_body`,
|
||||
:meth:`~HTTPException.get_headers` and :meth:`~HTTPException.get_response`
|
||||
methods. In any case you should have a look at the sourcecode of the
|
||||
exceptions module.
|
||||
|
||||
You can override the default description in the constructor with the
|
||||
``description`` parameter::
|
||||
|
||||
raise BadRequest(description='Request failed because X was not present')
|
|
@ -0,0 +1,169 @@
|
|||
==============
|
||||
HTTP Utilities
|
||||
==============
|
||||
|
||||
.. module:: werkzeug.http
|
||||
|
||||
Werkzeug provides a couple of functions to parse and generate HTTP headers
|
||||
that are useful when implementing WSGI middlewares or whenever you are
|
||||
operating on a lower level layer. All this functionality is also exposed
|
||||
from request and response objects.
|
||||
|
||||
|
||||
Datetime Functions
|
||||
==================
|
||||
|
||||
These functions simplify working with times in an HTTP context. Werkzeug
|
||||
produces timezone-aware :class:`~datetime.datetime` objects in UTC. When
|
||||
passing datetime objects to Werkzeug, it assumes any naive datetime is
|
||||
in UTC.
|
||||
|
||||
When comparing datetime values from Werkzeug, your own datetime objects
|
||||
must also be timezone-aware, or you must make the values from Werkzeug
|
||||
naive.
|
||||
|
||||
* ``dt = datetime.now(timezone.utc)`` gets the current time in UTC.
|
||||
* ``dt = datetime(..., tzinfo=timezone.utc)`` creates a time in UTC.
|
||||
* ``dt = dt.replace(tzinfo=timezone.utc)`` makes a naive object aware
|
||||
by assuming it's in UTC.
|
||||
* ``dt = dt.replace(tzinfo=None)`` makes an aware object naive.
|
||||
|
||||
.. autofunction:: parse_date
|
||||
|
||||
.. autofunction:: http_date
|
||||
|
||||
|
||||
Header Parsing
|
||||
==============
|
||||
|
||||
The following functions can be used to parse incoming HTTP headers.
|
||||
Because Python does not provide data structures with the semantics required
|
||||
by :rfc:`2616`, Werkzeug implements some custom data structures that are
|
||||
:ref:`documented separately <http-datastructures>`.
|
||||
|
||||
.. autofunction:: parse_options_header
|
||||
|
||||
.. autofunction:: parse_set_header
|
||||
|
||||
.. autofunction:: parse_list_header
|
||||
|
||||
.. autofunction:: parse_dict_header
|
||||
|
||||
.. autofunction:: parse_accept_header(value, [class])
|
||||
|
||||
.. autofunction:: parse_cache_control_header
|
||||
|
||||
.. autofunction:: parse_authorization_header
|
||||
|
||||
.. autofunction:: parse_www_authenticate_header
|
||||
|
||||
.. autofunction:: parse_if_range_header
|
||||
|
||||
.. autofunction:: parse_range_header
|
||||
|
||||
.. autofunction:: parse_content_range_header
|
||||
|
||||
Header Utilities
|
||||
================
|
||||
|
||||
The following utilities operate on HTTP headers well but do not parse
|
||||
them. They are useful if you're dealing with conditional responses or if
|
||||
you want to proxy arbitrary requests but want to remove WSGI-unsupported
|
||||
hop-by-hop headers. Also there is a function to create HTTP header
|
||||
strings from the parsed data.
|
||||
|
||||
.. autofunction:: is_entity_header
|
||||
|
||||
.. autofunction:: is_hop_by_hop_header
|
||||
|
||||
.. autofunction:: remove_entity_headers
|
||||
|
||||
.. autofunction:: remove_hop_by_hop_headers
|
||||
|
||||
.. autofunction:: is_byte_range_valid
|
||||
|
||||
.. autofunction:: quote_header_value
|
||||
|
||||
.. autofunction:: unquote_header_value
|
||||
|
||||
.. autofunction:: dump_header
|
||||
|
||||
|
||||
Cookies
|
||||
=======
|
||||
|
||||
.. autofunction:: parse_cookie
|
||||
|
||||
.. autofunction:: dump_cookie
|
||||
|
||||
|
||||
Conditional Response Helpers
|
||||
============================
|
||||
|
||||
For conditional responses the following functions might be useful:
|
||||
|
||||
.. autofunction:: parse_etags
|
||||
|
||||
.. autofunction:: quote_etag
|
||||
|
||||
.. autofunction:: unquote_etag
|
||||
|
||||
.. autofunction:: generate_etag
|
||||
|
||||
.. autofunction:: is_resource_modified
|
||||
|
||||
Constants
|
||||
=========
|
||||
|
||||
.. data:: HTTP_STATUS_CODES
|
||||
|
||||
A dict of status code -> default status message pairs. This is used
|
||||
by the wrappers and other places where an integer status code is expanded
|
||||
to a string throughout Werkzeug.
|
||||
|
||||
Form Data Parsing
|
||||
=================
|
||||
|
||||
.. module:: werkzeug.formparser
|
||||
|
||||
Werkzeug provides the form parsing functions separately from the request
|
||||
object so that you can access form data from a plain WSGI environment.
|
||||
|
||||
The following formats are currently supported by the form data parser:
|
||||
|
||||
- `application/x-www-form-urlencoded`
|
||||
- `multipart/form-data`
|
||||
|
||||
Nested multipart is not currently supported (Werkzeug 0.9), but it isn't used
|
||||
by any of the modern web browsers.
|
||||
|
||||
Usage example:
|
||||
|
||||
>>> from io import BytesIO
|
||||
>>> from werkzeug.formparser import parse_form_data
|
||||
>>> data = (
|
||||
... b'--foo\r\nContent-Disposition: form-data; name="test"\r\n'
|
||||
... b"\r\nHello World!\r\n--foo--"
|
||||
... )
|
||||
>>> environ = {
|
||||
... "wsgi.input": BytesIO(data),
|
||||
... "CONTENT_LENGTH": str(len(data)),
|
||||
... "CONTENT_TYPE": "multipart/form-data; boundary=foo",
|
||||
... "REQUEST_METHOD": "POST",
|
||||
... }
|
||||
>>> stream, form, files = parse_form_data(environ)
|
||||
>>> stream.read()
|
||||
b''
|
||||
>>> form['test']
|
||||
'Hello World!'
|
||||
>>> not files
|
||||
True
|
||||
|
||||
Normally the WSGI environment is provided by the WSGI gateway with the
|
||||
incoming data as part of it. If you want to generate such fake-WSGI
|
||||
environments for unittesting you might want to use the
|
||||
:func:`create_environ` function or the :class:`EnvironBuilder` instead.
|
||||
|
||||
.. autoclass:: FormDataParser
|
||||
|
||||
.. autofunction:: parse_form_data
|
|
@ -0,0 +1,78 @@
|
|||
Werkzeug
|
||||
========
|
||||
|
||||
*werkzeug* German noun: "tool".
|
||||
Etymology: *werk* ("work"), *zeug* ("stuff")
|
||||
|
||||
Werkzeug is a comprehensive `WSGI`_ web application library. It began as
|
||||
a simple collection of various utilities for WSGI applications and has
|
||||
become one of the most advanced WSGI utility libraries.
|
||||
|
||||
Werkzeug doesn't enforce any dependencies. It is up to the developer to
|
||||
choose a template engine, database adapter, and even how to handle
|
||||
requests.
|
||||
|
||||
.. _WSGI: https://wsgi.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
tutorial
|
||||
levels
|
||||
quickstart
|
||||
|
||||
|
||||
Serving and Testing
|
||||
-------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
serving
|
||||
test
|
||||
debug
|
||||
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
wrappers
|
||||
routing
|
||||
wsgi
|
||||
http
|
||||
datastructures
|
||||
utils
|
||||
urls
|
||||
local
|
||||
middleware/index
|
||||
exceptions
|
||||
|
||||
|
||||
Deployment
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
deployment/index
|
||||
|
||||
|
||||
Additional Information
|
||||
----------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
terms
|
||||
unicode
|
||||
request_data
|
||||
license
|
||||
changes
|
|
@ -0,0 +1,111 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
|
||||
Python Version
|
||||
--------------
|
||||
|
||||
We recommend using the latest version of Python. Werkzeug supports
|
||||
Python 3.7 and newer.
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
Werkzeug does not have any direct dependencies.
|
||||
|
||||
|
||||
Optional dependencies
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These distributions will not be installed automatically. Werkzeug will
|
||||
detect and use them if you install them.
|
||||
|
||||
* `Colorama`_ provides request log highlighting when using the
|
||||
development server on Windows. This works automatically on other
|
||||
systems.
|
||||
* `Watchdog`_ provides a faster, more efficient reloader for the
|
||||
development server.
|
||||
|
||||
.. _Colorama: https://pypi.org/project/colorama/
|
||||
.. _Watchdog: https://pypi.org/project/watchdog/
|
||||
|
||||
|
||||
greenlet
|
||||
~~~~~~~~
|
||||
|
||||
You may choose to use gevent or eventlet with your application. In this
|
||||
case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
|
||||
required.
|
||||
|
||||
These are not minimum supported versions, they only indicate the first
|
||||
versions that added necessary features. You should use the latest
|
||||
versions of each.
|
||||
|
||||
|
||||
Virtual environments
|
||||
--------------------
|
||||
|
||||
Use a virtual environment to manage the dependencies for your project,
|
||||
both in development and in production.
|
||||
|
||||
What problem does a virtual environment solve? The more Python
|
||||
projects you have, the more likely it is that you need to work with
|
||||
different versions of Python libraries, or even Python itself. Newer
|
||||
versions of libraries for one project can break compatibility in
|
||||
another project.
|
||||
|
||||
Virtual environments are independent groups of Python libraries, one for
|
||||
each project. Packages installed for one project will not affect other
|
||||
projects or the operating system's packages.
|
||||
|
||||
Python comes bundled with the :mod:`venv` module to create virtual
|
||||
environments.
|
||||
|
||||
|
||||
Create an environment
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create a project folder and a :file:`venv` folder within:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
mkdir myproject
|
||||
cd myproject
|
||||
python3 -m venv venv
|
||||
|
||||
On Windows:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
py -3 -m venv venv
|
||||
|
||||
|
||||
Activate the environment
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before you work on your project, activate the corresponding environment:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
. venv/bin/activate
|
||||
|
||||
On Windows:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
venv\Scripts\activate
|
||||
|
||||
Your shell prompt will change to show the name of the activated
|
||||
environment.
|
||||
|
||||
|
||||
Install Werkzeug
|
||||
----------------
|
||||
|
||||
Within the activated environment, use the following command to install
|
||||
Werkzeug:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install Werkzeug
|
|
@ -0,0 +1,72 @@
|
|||
==========
|
||||
API Levels
|
||||
==========
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
Werkzeug is intended to be a utility rather than a framework. Because of that
|
||||
the user-friendly API is separated from the lower-level API so that Werkzeug
|
||||
can easily be used to extend another system.
|
||||
|
||||
All the functionality the :class:`Request` and :class:`Response` objects (aka
|
||||
the "wrappers") provide is also available in small utility functions.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
This example implements a small `Hello World` application that greets the
|
||||
user with the name entered.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from markupsafe import escape
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
@Request.application
|
||||
def hello_world(request):
|
||||
result = ['<title>Greeter</title>']
|
||||
if request.method == 'POST':
|
||||
result.append(f"<h1>Hello {escape(request.form['name'])}!</h1>")
|
||||
result.append('''
|
||||
<form action="" method="post">
|
||||
<p>Name: <input type="text" name="name" size="20">
|
||||
<input type="submit" value="Greet me">
|
||||
</form>
|
||||
''')
|
||||
return Response(''.join(result), mimetype='text/html')
|
||||
|
||||
Alternatively the same application could be used without request and response
|
||||
objects but by taking advantage of the parsing functions werkzeug provides::
|
||||
|
||||
from markupsafe import escape
|
||||
from werkzeug.formparser import parse_form_data
|
||||
|
||||
def hello_world(environ, start_response):
|
||||
result = ['<title>Greeter</title>']
|
||||
if environ['REQUEST_METHOD'] == 'POST':
|
||||
form = parse_form_data(environ)[1]
|
||||
result.append(f"<h1>Hello {escape(form['name'])}!</h1>")
|
||||
result.append('''
|
||||
<form action="" method="post">
|
||||
<p>Name: <input type="text" name="name" size="20">
|
||||
<input type="submit" value="Greet me">
|
||||
</form>
|
||||
''')
|
||||
start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
|
||||
return [''.join(result).encode('utf-8')]
|
||||
|
||||
High or Low?
|
||||
============
|
||||
|
||||
Usually you want to use the high-level layer (the request and response
|
||||
objects). But there are situations where this might not be what you want.
|
||||
|
||||
For example you might be maintaining code for an application written in
|
||||
Django or another framework and you have to parse HTTP headers. You can
|
||||
utilize Werkzeug for that by accessing the lower-level HTTP header parsing
|
||||
functions.
|
||||
|
||||
Another situation where the low level parsing functions can be useful are
|
||||
custom WSGI frameworks, unit-testing or modernizing an old CGI/mod_python
|
||||
application to WSGI as well as WSGI middlewares where you want to keep the
|
||||
overhead low.
|
|
@ -0,0 +1,4 @@
|
|||
BSD-3-Clause License
|
||||
====================
|
||||
|
||||
.. include:: ../LICENSE.rst
|
|
@ -0,0 +1,110 @@
|
|||
Context Locals
|
||||
==============
|
||||
|
||||
.. module:: werkzeug.local
|
||||
|
||||
You may find that you have some data during each request that you want
|
||||
to use across functions. Instead of passing these as arguments between
|
||||
every function, you may want to access them as global data. However,
|
||||
using global variables in Python web applications is not thread safe;
|
||||
different workers might interfere with each others' data.
|
||||
|
||||
Instead of storing common data during a request using global variables,
|
||||
you must use context-local variables instead. A context local is
|
||||
defined/imported globally, but the data it contains is specific to the
|
||||
current thread, asyncio task, or greenlet. You won't accidentally get
|
||||
or overwrite another worker's data.
|
||||
|
||||
The current approach for storing per-context data in Python is the
|
||||
:class:`contextvars` module. Context vars store data per thread, async
|
||||
task, or greenlet. This replaces the older :class:`threading.local`
|
||||
which only handled threads.
|
||||
|
||||
Werkzeug provides wrappers around :class:`~contextvars.ContextVar` to
|
||||
make it easier to work with.
|
||||
|
||||
|
||||
Proxy Objects
|
||||
=============
|
||||
|
||||
:class:`LocalProxy` allows treating a context var as an object directly
|
||||
instead of needing to use and check
|
||||
:meth:`ContextVar.get() <contextvars.ContextVar.get>`. If the context
|
||||
var is set, the local proxy will look and behave like the object the var
|
||||
is set to. If it's not set, a ``RuntimeError`` is raised for most
|
||||
operations.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextvars import ContextVar
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
_request_var = ContextVar("request")
|
||||
request = LocalProxy(_request_var)
|
||||
|
||||
from werkzeug.wrappers import Request
|
||||
|
||||
@Request.application
|
||||
def app(r):
|
||||
_request_var.set(r)
|
||||
check_auth()
|
||||
...
|
||||
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
def check_auth():
|
||||
if request.form["username"] != "admin":
|
||||
raise Unauthorized()
|
||||
|
||||
Accessing ``request`` will point to the specific request that each
|
||||
server worker is handling. You can treat ``request`` just like an actual
|
||||
``Request`` object.
|
||||
|
||||
``bool(proxy)`` will always return ``False`` if the var is not set. If
|
||||
you need access to the object directly instead of the proxy, you can get
|
||||
it with the :meth:`~LocalProxy._get_current_object` method.
|
||||
|
||||
.. autoclass:: LocalProxy
|
||||
:members: _get_current_object
|
||||
|
||||
|
||||
Stacks and Namespaces
|
||||
=====================
|
||||
|
||||
:class:`~contextvars.ContextVar` stores one value at a time. You may
|
||||
find that you need to store a stack of items, or a namespace with
|
||||
multiple attributes. A list or dict can be used for these, but using
|
||||
them as context var values requires some extra care. Werkzeug provides
|
||||
:class:`LocalStack` which wraps a list, and :class:`Local` which wraps a
|
||||
dict.
|
||||
|
||||
There is some amount of performance penalty associated with these
|
||||
objects. Because lists and dicts are mutable, :class:`LocalStack` and
|
||||
:class:`Local` need to do extra work to ensure data isn't shared between
|
||||
nested contexts. If possible, design your application to use
|
||||
:class:`LocalProxy` around a context var directly.
|
||||
|
||||
.. autoclass:: LocalStack
|
||||
:members: push, pop, top, __call__
|
||||
|
||||
.. autoclass:: Local
|
||||
:members: __call__
|
||||
|
||||
|
||||
Releasing Data
|
||||
==============
|
||||
|
||||
A previous implementation of ``Local`` used internal data structures
|
||||
which could not be cleaned up automatically when each context ended.
|
||||
Instead, the following utilities could be used to release the data.
|
||||
|
||||
.. warning::
|
||||
|
||||
This should not be needed with the modern implementation, as the
|
||||
data in context vars is automatically managed by Python. It is kept
|
||||
for compatibility for now, but may be removed in the future.
|
||||
|
||||
.. autoclass:: LocalManager
|
||||
:members: cleanup, make_middleware, middleware
|
||||
|
||||
.. autofunction:: release_local
|
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.dispatcher
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.http_proxy
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.lint
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.profiler
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.proxy_fix
|
|
@ -0,0 +1 @@
|
|||
.. automodule:: werkzeug.middleware.shared_data
|
|
@ -0,0 +1,301 @@
|
|||
Quickstart
|
||||
==========
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
This part of the documentation shows how to use the most important parts of
|
||||
Werkzeug. It's intended as a starting point for developers with basic
|
||||
understanding of :pep:`3333` (WSGI) and :rfc:`2616` (HTTP).
|
||||
|
||||
|
||||
WSGI Environment
|
||||
================
|
||||
|
||||
The WSGI environment contains all the information the user request transmits
|
||||
to the application. It is passed to the WSGI application but you can also
|
||||
create a WSGI environ dict using the :func:`create_environ` helper:
|
||||
|
||||
>>> from werkzeug.test import create_environ
|
||||
>>> environ = create_environ('/foo', 'http://localhost:8080/')
|
||||
|
||||
Now we have an environment to play around:
|
||||
|
||||
>>> environ['PATH_INFO']
|
||||
'/foo'
|
||||
>>> environ['SCRIPT_NAME']
|
||||
''
|
||||
>>> environ['SERVER_NAME']
|
||||
'localhost'
|
||||
|
||||
Usually nobody wants to work with the environ directly because it uses a
|
||||
confusing string encoding scheme, and it does not provide any way to
|
||||
access the form data besides parsing that data by hand.
|
||||
|
||||
|
||||
Enter Request
|
||||
=============
|
||||
|
||||
For access to the request data the :class:`Request` object is much more fun.
|
||||
It wraps the `environ` and provides a read-only access to the data from
|
||||
there:
|
||||
|
||||
>>> from werkzeug.wrappers import Request
|
||||
>>> request = Request(environ)
|
||||
|
||||
Now you can access the important variables and Werkzeug will parse them
|
||||
for you and decode them where it makes sense. The default charset for
|
||||
requests is set to `utf-8` but you can change that by subclassing
|
||||
:class:`Request`.
|
||||
|
||||
>>> request.path
|
||||
'/foo'
|
||||
>>> request.script_root
|
||||
''
|
||||
>>> request.host
|
||||
'localhost:8080'
|
||||
>>> request.url
|
||||
'http://localhost:8080/foo'
|
||||
|
||||
We can also find out which HTTP method was used for the request:
|
||||
|
||||
>>> request.method
|
||||
'GET'
|
||||
|
||||
This way we can also access URL arguments (the query string) and data that
|
||||
was transmitted in a POST/PUT request.
|
||||
|
||||
For testing purposes we can create a request object from supplied data
|
||||
using the :meth:`~Request.from_values` method:
|
||||
|
||||
>>> from io import StringIO
|
||||
>>> data = "name=this+is+encoded+form+data&another_key=another+one"
|
||||
>>> request = Request.from_values(query_string='foo=bar&blah=blafasel',
|
||||
... content_length=len(data), input_stream=StringIO(data),
|
||||
... content_type='application/x-www-form-urlencoded',
|
||||
... method='POST')
|
||||
...
|
||||
>>> request.method
|
||||
'POST'
|
||||
|
||||
Now we can access the URL parameters easily:
|
||||
|
||||
>>> request.args.keys()
|
||||
['blah', 'foo']
|
||||
>>> request.args['blah']
|
||||
'blafasel'
|
||||
|
||||
Same for the supplied form data:
|
||||
|
||||
>>> request.form['name']
|
||||
'this is encoded form data'
|
||||
|
||||
Handling for uploaded files is not much harder as you can see from this
|
||||
example::
|
||||
|
||||
def store_file(request):
|
||||
file = request.files.get('my_file')
|
||||
if file:
|
||||
file.save('/where/to/store/the/file.txt')
|
||||
else:
|
||||
handle_the_error()
|
||||
|
||||
The files are represented as :class:`FileStorage` objects which provide
|
||||
some common operations to work with them.
|
||||
|
||||
Request headers can be accessed by using the :class:`~Request.headers`
|
||||
attribute:
|
||||
|
||||
>>> request.headers['Content-Length']
|
||||
'54'
|
||||
>>> request.headers['Content-Type']
|
||||
'application/x-www-form-urlencoded'
|
||||
|
||||
The keys for the headers are of course case insensitive.
|
||||
|
||||
|
||||
Header Parsing
|
||||
==============
|
||||
|
||||
There is more. Werkzeug provides convenient access to often used HTTP headers
|
||||
and other request data.
|
||||
|
||||
Let's create a request object with all the data a typical web browser transmits
|
||||
so that we can play with it:
|
||||
|
||||
>>> environ = create_environ()
|
||||
>>> environ.update(
|
||||
... HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
... HTTP_ACCEPT_LANGUAGE='de-at,en-us;q=0.8,en;q=0.5',
|
||||
... HTTP_ACCEPT_ENCODING='gzip,deflate',
|
||||
... HTTP_ACCEPT_CHARSET='ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
||||
... HTTP_IF_MODIFIED_SINCE='Fri, 20 Feb 2009 10:10:25 GMT',
|
||||
... HTTP_IF_NONE_MATCH='"e51c9-1e5d-46356dc86c640"',
|
||||
... HTTP_CACHE_CONTROL='max-age=0'
|
||||
... )
|
||||
...
|
||||
>>> request = Request(environ)
|
||||
|
||||
With the accept header the browser informs the web application what
|
||||
mimetypes it can handle and how well. All accept headers are sorted by
|
||||
the quality, the best item being the first:
|
||||
|
||||
>>> request.accept_mimetypes.best
|
||||
'text/html'
|
||||
>>> 'application/xhtml+xml' in request.accept_mimetypes
|
||||
True
|
||||
>>> print request.accept_mimetypes["application/json"]
|
||||
0.8
|
||||
|
||||
The same works for languages:
|
||||
|
||||
>>> request.accept_languages.best
|
||||
'de-at'
|
||||
>>> request.accept_languages.values()
|
||||
['de-at', 'en-us', 'en']
|
||||
|
||||
And of course encodings and charsets:
|
||||
|
||||
>>> 'gzip' in request.accept_encodings
|
||||
True
|
||||
>>> request.accept_charsets.best
|
||||
'ISO-8859-1'
|
||||
>>> 'utf-8' in request.accept_charsets
|
||||
True
|
||||
|
||||
Normalization is available, so you can safely use alternative forms to perform
|
||||
containment checking:
|
||||
|
||||
>>> 'UTF8' in request.accept_charsets
|
||||
True
|
||||
>>> 'de_AT' in request.accept_languages
|
||||
True
|
||||
|
||||
E-tags and other conditional headers are available in parsed form as well:
|
||||
|
||||
>>> request.if_modified_since
|
||||
datetime.datetime(2009, 2, 20, 10, 10, 25, tzinfo=datetime.timezone.utc)
|
||||
>>> request.if_none_match
|
||||
<ETags '"e51c9-1e5d-46356dc86c640"'>
|
||||
>>> request.cache_control
|
||||
<RequestCacheControl 'max-age=0'>
|
||||
>>> request.cache_control.max_age
|
||||
0
|
||||
>>> 'e51c9-1e5d-46356dc86c640' in request.if_none_match
|
||||
True
|
||||
|
||||
|
||||
Responses
|
||||
=========
|
||||
|
||||
Response objects are the opposite of request objects. They are used to send
|
||||
data back to the client. In reality, response objects are nothing more than
|
||||
glorified WSGI applications.
|
||||
|
||||
So what you are doing is not *returning* the response objects from your WSGI
|
||||
application but *calling* it as WSGI application inside your WSGI application
|
||||
and returning the return value of that call.
|
||||
|
||||
So imagine your standard WSGI "Hello World" application::
|
||||
|
||||
def application(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return ['Hello World!']
|
||||
|
||||
With response objects it would look like this::
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
def application(environ, start_response):
|
||||
response = Response('Hello World!')
|
||||
return response(environ, start_response)
|
||||
|
||||
Also, unlike request objects, response objects are designed to be modified.
|
||||
So here is what you can do with them:
|
||||
|
||||
>>> from werkzeug.wrappers import Response
|
||||
>>> response = Response("Hello World!")
|
||||
>>> response.headers['content-type']
|
||||
'text/plain; charset=utf-8'
|
||||
>>> response.data
|
||||
'Hello World!'
|
||||
>>> response.headers['content-length'] = len(response.data)
|
||||
|
||||
You can modify the status of the response in the same way. Either just the
|
||||
code or provide a message as well:
|
||||
|
||||
>>> response.status
|
||||
'200 OK'
|
||||
>>> response.status = '404 Not Found'
|
||||
>>> response.status_code
|
||||
404
|
||||
>>> response.status_code = 400
|
||||
>>> response.status
|
||||
'400 BAD REQUEST'
|
||||
|
||||
As you can see attributes work in both directions. So you can set both
|
||||
:attr:`~Response.status` and :attr:`~Response.status_code` and the
|
||||
change will be reflected to the other.
|
||||
|
||||
Also common headers are exposed as attributes or with methods to set /
|
||||
retrieve them:
|
||||
|
||||
>>> response.content_length
|
||||
12
|
||||
>>> from datetime import datetime, timezone
|
||||
>>> response.date = datetime(2009, 2, 20, 17, 42, 51, tzinfo=timezone.utc)
|
||||
>>> response.headers['Date']
|
||||
'Fri, 20 Feb 2009 17:42:51 GMT'
|
||||
|
||||
Because etags can be weak or strong there are methods to set them:
|
||||
|
||||
>>> response.set_etag("12345-abcd")
|
||||
>>> response.headers['etag']
|
||||
'"12345-abcd"'
|
||||
>>> response.get_etag()
|
||||
('12345-abcd', False)
|
||||
>>> response.set_etag("12345-abcd", weak=True)
|
||||
>>> response.get_etag()
|
||||
('12345-abcd', True)
|
||||
|
||||
Some headers are available as mutable structures. For example most
|
||||
of the `Content-` headers are sets of values:
|
||||
|
||||
>>> response.content_language.add('en-us')
|
||||
>>> response.content_language.add('en')
|
||||
>>> response.headers['Content-Language']
|
||||
'en-us, en'
|
||||
|
||||
Also here this works in both directions:
|
||||
|
||||
>>> response.headers['Content-Language'] = 'de-AT, de'
|
||||
>>> response.content_language
|
||||
HeaderSet(['de-AT', 'de'])
|
||||
|
||||
Authentication headers can be set that way as well:
|
||||
|
||||
>>> response.www_authenticate.set_basic("My protected resource")
|
||||
>>> response.headers['www-authenticate']
|
||||
'Basic realm="My protected resource"'
|
||||
|
||||
Cookies can be set as well:
|
||||
|
||||
>>> response.set_cookie('name', 'value')
|
||||
>>> response.headers['Set-Cookie']
|
||||
'name=value; Path=/'
|
||||
>>> response.set_cookie('name2', 'value2')
|
||||
|
||||
If headers appear multiple times you can use the :meth:`~Headers.getlist`
|
||||
method to get all values for a header:
|
||||
|
||||
>>> response.headers.getlist('Set-Cookie')
|
||||
['name=value; Path=/', 'name2=value2; Path=/']
|
||||
|
||||
Finally if you have set all the conditional values, you can make the
|
||||
response conditional against a request. Which means that if the request
|
||||
can assure that it has the information already, no data besides the headers
|
||||
is sent over the network which saves traffic. For that you should set at
|
||||
least an etag (which is used for comparison) and the date header and then
|
||||
call :class:`~Request.make_conditional` with the request object.
|
||||
|
||||
The response is modified accordingly (status code changed, response body
|
||||
removed, entity headers removed etc.)
|
|
@ -0,0 +1,100 @@
|
|||
Dealing with Request Data
|
||||
=========================
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
The most important rule about web development is "Do not trust the user".
|
||||
This is especially true for incoming request data on the input stream.
|
||||
With WSGI this is actually a bit harder than you would expect. Because
|
||||
of that Werkzeug wraps the request stream for you to save you from the
|
||||
most prominent problems with it.
|
||||
|
||||
|
||||
Missing EOF Marker on Input Stream
|
||||
----------------------------------
|
||||
|
||||
The input stream has no end-of-file marker. If you would call the
|
||||
:meth:`~file.read` method on the `wsgi.input` stream you would cause your
|
||||
application to hang on conforming servers. This is actually intentional
|
||||
however painful. Werkzeug solves that problem by wrapping the input
|
||||
stream in a special :class:`LimitedStream`. The input stream is exposed
|
||||
on the request objects as :attr:`~Request.stream`. This one is either
|
||||
an empty stream (if the form data was parsed) or a limited stream with
|
||||
the contents of the input stream.
|
||||
|
||||
|
||||
When does Werkzeug Parse?
|
||||
-------------------------
|
||||
|
||||
Werkzeug parses the incoming data under the following situations:
|
||||
|
||||
- you access either :attr:`~Request.form`, :attr:`~Request.files`,
|
||||
or :attr:`~Request.stream` and the request method was
|
||||
`POST` or `PUT`.
|
||||
- if you call :func:`parse_form_data`.
|
||||
|
||||
These calls are not interchangeable. If you invoke :func:`parse_form_data`
|
||||
you must not use the request object or at least not the attributes that
|
||||
trigger the parsing process.
|
||||
|
||||
This is also true if you read from the `wsgi.input` stream before the
|
||||
parsing.
|
||||
|
||||
**General rule:** Leave the WSGI input stream alone. Especially in
|
||||
WSGI middlewares. Use either the parsing functions or the request
|
||||
object. Do not mix multiple WSGI utility libraries for form data
|
||||
parsing or anything else that works on the input stream.
|
||||
|
||||
|
||||
How does it Parse?
|
||||
------------------
|
||||
|
||||
The standard Werkzeug parsing behavior handles three cases:
|
||||
|
||||
- input content type was `multipart/form-data`. In this situation the
|
||||
:class:`~Request.stream` will be empty and
|
||||
:class:`~Request.form` will contain the regular `POST` / `PUT`
|
||||
data, :class:`~Request.files` will contain the uploaded
|
||||
files as :class:`FileStorage` objects.
|
||||
- input content type was `application/x-www-form-urlencoded`. Then the
|
||||
:class:`~Request.stream` will be empty and
|
||||
:class:`~Request.form` will contain the regular `POST` / `PUT`
|
||||
data and :class:`~Request.files` will be empty.
|
||||
- the input content type was neither of them, :class:`~Request.stream`
|
||||
points to a :class:`LimitedStream` with the input data for further
|
||||
processing.
|
||||
|
||||
Special note on the :attr:`~Request.get_data` method: Calling this
|
||||
loads the full request data into memory. This is only safe to do if the
|
||||
:attr:`~Request.max_content_length` is set. Also you can *either*
|
||||
read the stream *or* call :meth:`~Request.get_data`.
|
||||
|
||||
|
||||
Limiting Request Data
|
||||
---------------------
|
||||
|
||||
To avoid being the victim of a DDOS attack you can set the maximum
|
||||
accepted content length and request field sizes. The :class:`Request`
|
||||
class has two attributes for that: :attr:`~Request.max_content_length`
|
||||
and :attr:`~Request.max_form_memory_size`.
|
||||
|
||||
The first one can be used to limit the total content length. For example
|
||||
by setting it to ``1024 * 1024 * 16`` the request won't accept more than
|
||||
16MB of transmitted data.
|
||||
|
||||
Because certain data can't be moved to the hard disk (regular post data)
|
||||
whereas temporary files can, there is a second limit you can set. The
|
||||
:attr:`~Request.max_form_memory_size` limits the size of `POST`
|
||||
transmitted form data. By setting it to ``1024 * 1024 * 2`` you can make
|
||||
sure that all in memory-stored fields are not more than 2MB in size.
|
||||
|
||||
This however does *not* affect in-memory stored files if the
|
||||
`stream_factory` used returns a in-memory file.
|
||||
|
||||
|
||||
How to extend Parsing?
|
||||
----------------------
|
||||
|
||||
Modern web applications transmit a lot more than multipart form data or
|
||||
url encoded data. To extend the capabilities, subclass :class:`Request`
|
||||
or :class:`Request` and add or extend methods.
|
|
@ -0,0 +1,307 @@
|
|||
===========
|
||||
URL Routing
|
||||
===========
|
||||
|
||||
.. module:: werkzeug.routing
|
||||
|
||||
When it comes to combining multiple controller or view functions (however
|
||||
you want to call them), you need a dispatcher. A simple way would be
|
||||
applying regular expression tests on ``PATH_INFO`` and call registered
|
||||
callback functions that return the value.
|
||||
|
||||
Werkzeug provides a much more powerful system, similar to `Routes`_. All the
|
||||
objects mentioned on this page must be imported from :mod:`werkzeug.routing`, not
|
||||
from :mod:`werkzeug`!
|
||||
|
||||
.. _Routes: https://routes.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
Here is a simple example which could be the URL definition for a blog::
|
||||
|
||||
from werkzeug.routing import Map, Rule, NotFound, RequestRedirect
|
||||
|
||||
url_map = Map([
|
||||
Rule('/', endpoint='blog/index'),
|
||||
Rule('/<int:year>/', endpoint='blog/archive'),
|
||||
Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),
|
||||
Rule('/<int:year>/<int:month>/<int:day>/', endpoint='blog/archive'),
|
||||
Rule('/<int:year>/<int:month>/<int:day>/<slug>',
|
||||
endpoint='blog/show_post'),
|
||||
Rule('/about', endpoint='blog/about_me'),
|
||||
Rule('/feeds/', endpoint='blog/feeds'),
|
||||
Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
|
||||
])
|
||||
|
||||
def application(environ, start_response):
|
||||
urls = url_map.bind_to_environ(environ)
|
||||
try:
|
||||
endpoint, args = urls.match()
|
||||
except HTTPException, e:
|
||||
return e(environ, start_response)
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return [f'Rule points to {endpoint!r} with arguments {args!r}'.encode()]
|
||||
|
||||
So what does that do? First of all we create a new :class:`Map` which stores
|
||||
a bunch of URL rules. Then we pass it a list of :class:`Rule` objects.
|
||||
|
||||
Each :class:`Rule` object is instantiated with a string that represents a rule
|
||||
and an endpoint which will be the alias for what view the rule represents.
|
||||
Multiple rules can have the same endpoint, but should have different arguments
|
||||
to allow URL construction.
|
||||
|
||||
The format for the URL rules is straightforward, but explained in detail below.
|
||||
|
||||
Inside the WSGI application we bind the url_map to the current request which will
|
||||
return a new :class:`MapAdapter`. This url_map adapter can then be used to match
|
||||
or build domains for the current request.
|
||||
|
||||
The :meth:`MapAdapter.match` method can then either return a tuple in the form
|
||||
``(endpoint, args)`` or raise one of the three exceptions
|
||||
:exc:`~werkzeug.exceptions.NotFound`, :exc:`~werkzeug.exceptions.MethodNotAllowed`,
|
||||
or :exc:`~werkzeug.exceptions.RequestRedirect`. For more details about those
|
||||
exceptions have a look at the documentation of the :meth:`MapAdapter.match` method.
|
||||
|
||||
|
||||
Rule Format
|
||||
===========
|
||||
|
||||
Rule strings are URL paths with placeholders for variable parts in the
|
||||
format ``<converter(arguments):name>``. ``converter`` and ``arguments``
|
||||
(with parentheses) are optional. If no converter is given, the
|
||||
``default`` converter is used (``string`` by default). The available
|
||||
converters are discussed below.
|
||||
|
||||
Rules that end with a slash are "branches", others are "leaves". If
|
||||
``strict_slashes`` is enabled (the default), visiting a branch URL
|
||||
without a trailing slash will redirect to the URL with a slash appended.
|
||||
|
||||
Many HTTP servers merge consecutive slashes into one when receiving
|
||||
requests. If ``merge_slashes`` is enabled (the default), rules will
|
||||
merge slashes in non-variable parts when matching and building. Visiting
|
||||
a URL with consecutive slashes will redirect to the URL with slashes
|
||||
merged. If you want to disable ``merge_slashes`` for a :class:`Rule` or
|
||||
:class:`Map`, you'll also need to configure your web server
|
||||
appropriately.
|
||||
|
||||
|
||||
Built-in Converters
|
||||
===================
|
||||
|
||||
Converters for common types of URL variables are built-in. The available
|
||||
converters can be overridden or extended through :attr:`Map.converters`.
|
||||
|
||||
.. autoclass:: UnicodeConverter
|
||||
|
||||
.. autoclass:: PathConverter
|
||||
|
||||
.. autoclass:: AnyConverter
|
||||
|
||||
.. autoclass:: IntegerConverter
|
||||
|
||||
.. autoclass:: FloatConverter
|
||||
|
||||
.. autoclass:: UUIDConverter
|
||||
|
||||
|
||||
Maps, Rules and Adapters
|
||||
========================
|
||||
|
||||
.. autoclass:: Map
|
||||
:members:
|
||||
|
||||
.. attribute:: converters
|
||||
|
||||
The dictionary of converters. This can be modified after the class
|
||||
was created, but will only affect rules added after the
|
||||
modification. If the rules are defined with the list passed to the
|
||||
class, the `converters` parameter to the constructor has to be used
|
||||
instead.
|
||||
|
||||
.. autoclass:: MapAdapter
|
||||
:members:
|
||||
|
||||
.. autoclass:: Rule
|
||||
:members: empty
|
||||
|
||||
|
||||
Matchers
|
||||
========
|
||||
|
||||
.. autoclass:: StateMachineMatcher
|
||||
:members:
|
||||
|
||||
|
||||
Rule Factories
|
||||
==============
|
||||
|
||||
.. autoclass:: RuleFactory
|
||||
:members: get_rules
|
||||
|
||||
.. autoclass:: Subdomain
|
||||
|
||||
.. autoclass:: Submount
|
||||
|
||||
.. autoclass:: EndpointPrefix
|
||||
|
||||
|
||||
Rule Templates
|
||||
==============
|
||||
|
||||
.. autoclass:: RuleTemplate
|
||||
|
||||
|
||||
Custom Converters
|
||||
=================
|
||||
|
||||
You can add custom converters that add behaviors not provided by the
|
||||
built-in converters. To make a custom converter, subclass
|
||||
:class:`BaseConverter` then pass the new class to the :class:`Map`
|
||||
``converters`` parameter, or add it to
|
||||
:attr:`url_map.converters <Map.converters>`.
|
||||
|
||||
The converter should have a ``regex`` attribute with a regular
|
||||
expression to match with. If the converter can take arguments in a URL
|
||||
rule, it should accept them in its ``__init__`` method. The entire
|
||||
regex expression will be matched as a group and used as the value for
|
||||
conversion.
|
||||
|
||||
If a custom converter can match a forward slash, ``/``, it should have
|
||||
the attribute ``part_isolating`` set to ``False``. This will ensure
|
||||
that rules using the custom converter are correctly matched.
|
||||
|
||||
It can implement a ``to_python`` method to convert the matched string to
|
||||
some other object. This can also do extra validation that wasn't
|
||||
possible with the ``regex`` attribute, and should raise a
|
||||
:exc:`werkzeug.routing.ValidationError` in that case. Raising any other
|
||||
errors will cause a 500 error.
|
||||
|
||||
It can implement a ``to_url`` method to convert a Python object to a
|
||||
string when building a URL. Any error raised here will be converted to a
|
||||
:exc:`werkzeug.routing.BuildError` and eventually cause a 500 error.
|
||||
|
||||
This example implements a ``BooleanConverter`` that will match the
|
||||
strings ``"yes"``, ``"no"``, and ``"maybe"``, returning a random value
|
||||
for ``"maybe"``. ::
|
||||
|
||||
from random import randrange
|
||||
from werkzeug.routing import BaseConverter, ValidationError
|
||||
|
||||
class BooleanConverter(BaseConverter):
|
||||
regex = r"(?:yes|no|maybe)"
|
||||
|
||||
def __init__(self, url_map, maybe=False):
|
||||
super().__init__(url_map)
|
||||
self.maybe = maybe
|
||||
|
||||
def to_python(self, value):
|
||||
if value == "maybe":
|
||||
if self.maybe:
|
||||
return not randrange(2)
|
||||
raise ValidationError
|
||||
return value == 'yes'
|
||||
|
||||
def to_url(self, value):
|
||||
return "yes" if value else "no"
|
||||
|
||||
from werkzeug.routing import Map, Rule
|
||||
|
||||
url_map = Map([
|
||||
Rule("/vote/<bool:werkzeug_rocks>", endpoint="vote"),
|
||||
Rule("/guess/<bool(maybe=True):foo>", endpoint="guess")
|
||||
], converters={'bool': BooleanConverter})
|
||||
|
||||
If you want to change the default converter, assign a different
|
||||
converter to the ``"default"`` key.
|
||||
|
||||
|
||||
Host Matching
|
||||
=============
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
Starting with Werkzeug 0.7 it's also possible to do matching on the whole
|
||||
host names instead of just the subdomain. To enable this feature you need
|
||||
to pass ``host_matching=True`` to the :class:`Map` constructor and provide
|
||||
the `host` argument to all routes::
|
||||
|
||||
url_map = Map([
|
||||
Rule('/', endpoint='www_index', host='www.example.com'),
|
||||
Rule('/', endpoint='help_index', host='help.example.com')
|
||||
], host_matching=True)
|
||||
|
||||
Variable parts are of course also possible in the host section::
|
||||
|
||||
url_map = Map([
|
||||
Rule('/', endpoint='www_index', host='www.example.com'),
|
||||
Rule('/', endpoint='user_index', host='<user>.example.com')
|
||||
], host_matching=True)
|
||||
|
||||
|
||||
WebSockets
|
||||
==========
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
If a :class:`Rule` is created with ``websocket=True``, it will only
|
||||
match if the :class:`Map` is bound to a request with a ``url_scheme`` of
|
||||
``ws`` or ``wss``.
|
||||
|
||||
.. note::
|
||||
|
||||
Werkzeug has no further WebSocket support beyond routing. This
|
||||
functionality is mostly of use to ASGI projects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
url_map = Map([
|
||||
Rule("/ws", endpoint="comm", websocket=True),
|
||||
])
|
||||
adapter = map.bind("example.org", "/ws", url_scheme="ws")
|
||||
assert adapter.match() == ("comm", {})
|
||||
|
||||
If the only match is a WebSocket rule and the bind is HTTP (or the
|
||||
only match is HTTP and the bind is WebSocket) a
|
||||
:exc:`WebsocketMismatch` (derives from
|
||||
:exc:`~werkzeug.exceptions.BadRequest`) exception is raised.
|
||||
|
||||
As WebSocket URLs have a different scheme, rules are always built with a
|
||||
scheme and host, ``force_external=True`` is implied.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
url = adapter.build("comm")
|
||||
assert url == "ws://example.org/ws"
|
||||
|
||||
|
||||
State Machine Matching
|
||||
======================
|
||||
|
||||
The default matching algorithm uses a state machine that transitions
|
||||
between parts of the request path to find a match. To understand how
|
||||
this works consider this rule::
|
||||
|
||||
/resource/<id>
|
||||
|
||||
Firstly this rule is decomposed into two ``RulePart``. The first is a
|
||||
static part with a content equal to ``resource``, the second is
|
||||
dynamic and requires a regex match to ``[^/]+``.
|
||||
|
||||
A state machine is then created with an initial state that represents
|
||||
the rule's first ``/``. This initial state has a single, static
|
||||
transition to the next state which represents the rule's second
|
||||
``/``. This second state has a single dynamic transition to the final
|
||||
state which includes the rule.
|
||||
|
||||
To match a path the matcher starts and the initial state and follows
|
||||
transitions that work. Clearly a trial path of ``/resource/2`` has the
|
||||
parts ``""``, ``resource``, and ``2`` which match the transitions and
|
||||
hence a rule will match. Whereas ``/other/2`` will not match as there
|
||||
is no transition for the ``other`` part from the initial state.
|
||||
|
||||
The only diversion from this rule is if a ``RulePart`` is not
|
||||
part-isolating i.e. it will match ``/``. In this case the ``RulePart``
|
||||
is considered final and represents a transition that must include all
|
||||
the subsequent parts of the trial path.
|
|
@ -0,0 +1,267 @@
|
|||
=========================
|
||||
Serving WSGI Applications
|
||||
=========================
|
||||
|
||||
.. module:: werkzeug.serving
|
||||
|
||||
There are many ways to serve a WSGI application. While you're developing it,
|
||||
you usually don't want to have a full-blown webserver like Apache up and
|
||||
running, but instead a simple standalone one. Because of that Werkzeug comes
|
||||
with a builtin development server.
|
||||
|
||||
The easiest way is creating a small ``start-myproject.py`` file that runs the
|
||||
application using the builtin server::
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
from myproject import make_app
|
||||
|
||||
app = make_app(...)
|
||||
run_simple('localhost', 8080, app, use_reloader=True)
|
||||
|
||||
You can also pass it the `extra_files` keyword argument with a list of
|
||||
additional files (like configuration files) you want to observe.
|
||||
|
||||
.. autofunction:: run_simple
|
||||
|
||||
.. autofunction:: is_running_from_reloader
|
||||
|
||||
.. autofunction:: make_ssl_devcert
|
||||
|
||||
.. admonition:: Information
|
||||
|
||||
The development server is not intended to be used on production systems.
|
||||
It was designed especially for development purposes and performs poorly
|
||||
under high load. For deployment setups have a look at the
|
||||
:doc:`/deployment/index` pages.
|
||||
|
||||
.. _reloader:
|
||||
|
||||
Reloader
|
||||
--------
|
||||
|
||||
.. versionchanged:: 0.10
|
||||
|
||||
The Werkzeug reloader constantly monitors modules and paths of your web
|
||||
application, and restarts the server if any of the observed files change.
|
||||
|
||||
Since version 0.10, there are two backends the reloader supports: ``stat`` and
|
||||
``watchdog``.
|
||||
|
||||
- The default ``stat`` backend simply checks the ``mtime`` of all files in a
|
||||
regular interval. This is sufficient for most cases, however, it is known to
|
||||
drain a laptop's battery.
|
||||
|
||||
- The ``watchdog`` backend uses filesystem events, and is much faster than
|
||||
``stat``. It requires the `watchdog <https://pypi.org/project/watchdog/>`_
|
||||
module to be installed. The recommended way to achieve this is to add
|
||||
``Werkzeug[watchdog]`` to your requirements file.
|
||||
|
||||
If ``watchdog`` is installed and available it will automatically be used
|
||||
instead of the builtin ``stat`` reloader.
|
||||
|
||||
To switch between the backends you can use the `reloader_type` parameter of the
|
||||
:func:`run_simple` function. ``'stat'`` sets it to the default stat based
|
||||
polling and ``'watchdog'`` forces it to the watchdog backend.
|
||||
|
||||
.. note::
|
||||
|
||||
Some edge cases, like modules that failed to import correctly, are not
|
||||
handled by the stat reloader for performance reasons. The watchdog reloader
|
||||
monitors such files too.
|
||||
|
||||
|
||||
Colored Logging
|
||||
---------------
|
||||
|
||||
The development server highlights the request logs in different colors
|
||||
based on the status code. On Windows, `Colorama`_ must be installed as
|
||||
well to enable this.
|
||||
|
||||
.. _Colorama: https://pypi.org/project/colorama/
|
||||
|
||||
|
||||
Virtual Hosts
|
||||
-------------
|
||||
|
||||
Many web applications utilize multiple subdomains. This can be a bit tricky
|
||||
to simulate locally. Fortunately there is the `hosts file`_ that can be used
|
||||
to assign the local computer multiple names.
|
||||
|
||||
This allows you to call your local computer `yourapplication.local` and
|
||||
`api.yourapplication.local` (or anything else) in addition to `localhost`.
|
||||
|
||||
You can find the hosts file on the following location:
|
||||
|
||||
=============== ==============================================
|
||||
Windows ``%SystemRoot%\system32\drivers\etc\hosts``
|
||||
Linux / OS X ``/etc/hosts``
|
||||
=============== ==============================================
|
||||
|
||||
You can open the file with your favorite text editor and add a new name after
|
||||
`localhost`::
|
||||
|
||||
127.0.0.1 localhost yourapplication.local api.yourapplication.local
|
||||
|
||||
Save the changes and after a while you should be able to access the
|
||||
development server on these host names as well. You can use the
|
||||
:doc:`/routing` system to dispatch between different hosts or parse
|
||||
:attr:`request.host` yourself.
|
||||
|
||||
|
||||
Shutting Down The Server
|
||||
------------------------
|
||||
|
||||
In some cases it can be useful to shut down a server after handling a
|
||||
request. For example, a local command line tool that needs OAuth
|
||||
authentication could temporarily start a server to listen for a
|
||||
response, record the user's token, then stop the server.
|
||||
|
||||
One method to do this could be to start a server in a
|
||||
:mod:`multiprocessing` process, then terminate the process after a value
|
||||
is passed back to the parent.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import multiprocessing
|
||||
from werkzeug import Request, Response, run_simple
|
||||
|
||||
def get_token(q: multiprocessing.Queue) -> None:
|
||||
@Request.application
|
||||
def app(request: Request) -> Response:
|
||||
q.put(request.args["token"])
|
||||
return Response("", 204)
|
||||
|
||||
run_simple("localhost", 5000, app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
q = multiprocessing.Queue()
|
||||
p = multiprocessing.Process(target=get_token, args=(q,))
|
||||
p.start()
|
||||
print("waiting")
|
||||
token = q.get(block=True)
|
||||
p.terminate()
|
||||
print(token)
|
||||
|
||||
That example uses Werkzeug's development server, but any production
|
||||
server that can be started as a Python process could use the same
|
||||
technique and should be preferred for security. Another method could be
|
||||
to start a :mod:`subprocess` process and send the value back over
|
||||
``stdout``.
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
On operating systems that support ipv6 and have it configured such as modern
|
||||
Linux systems, OS X 10.4 or higher as well as Windows Vista some browsers can
|
||||
be painfully slow if accessing your local server. The reason for this is that
|
||||
sometimes "localhost" is configured to be available on both ipv4 and ipv6 sockets
|
||||
and some browsers will try to access ipv6 first and then ipv4.
|
||||
|
||||
At the current time the integrated webserver does not support ipv6 and ipv4 at
|
||||
the same time and for better portability ipv4 is the default.
|
||||
|
||||
If you notice that the web browser takes ages to load the page there are two ways
|
||||
around this issue. If you don't need ipv6 support you can disable the ipv6 entry
|
||||
in the `hosts file`_ by removing this line::
|
||||
|
||||
::1 localhost
|
||||
|
||||
Alternatively you can also disable ipv6 support in your browser. For example
|
||||
if Firefox shows this behavior you can disable it by going to ``about:config``
|
||||
and disabling the `network.dns.disableIPv6` key. This however is not
|
||||
recommended as of Werkzeug 0.6.1!
|
||||
|
||||
Starting with Werkzeug 0.6.1, the server will now switch between ipv4 and
|
||||
ipv6 based on your operating system's configuration. This means if that
|
||||
you disabled ipv6 support in your browser but your operating system is
|
||||
preferring ipv6, you will be unable to connect to your server. In that
|
||||
situation, you can either remove the localhost entry for ``::1`` or
|
||||
explicitly bind the hostname to an ipv4 address (`127.0.0.1`)
|
||||
|
||||
.. _hosts file: https://en.wikipedia.org/wiki/Hosts_file
|
||||
|
||||
SSL
|
||||
---
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
The builtin server supports SSL for testing purposes. If an SSL context is
|
||||
provided it will be used. That means a server can either run in HTTP or HTTPS
|
||||
mode, but not both.
|
||||
|
||||
Quickstart
|
||||
``````````
|
||||
|
||||
The easiest way to do SSL based development with Werkzeug is by using it
|
||||
to generate an SSL certificate and private key and storing that somewhere
|
||||
and to then put it there. For the certificate you need to provide the
|
||||
name of your server on generation or a `CN`.
|
||||
|
||||
1. Generate an SSL key and store it somewhere:
|
||||
|
||||
>>> from werkzeug.serving import make_ssl_devcert
|
||||
>>> make_ssl_devcert('/path/to/the/key', host='localhost')
|
||||
('/path/to/the/key.crt', '/path/to/the/key.key')
|
||||
|
||||
2. Now this tuple can be passed as ``ssl_context`` to the
|
||||
:func:`run_simple` method::
|
||||
|
||||
run_simple('localhost', 4000, application,
|
||||
ssl_context=('/path/to/the/key.crt',
|
||||
'/path/to/the/key.key'))
|
||||
|
||||
You will have to acknowledge the certificate in your browser once then.
|
||||
|
||||
Loading Contexts by Hand
|
||||
````````````````````````
|
||||
|
||||
You can use a ``ssl.SSLContext`` object instead of a tuple for full
|
||||
control over the TLS configuration.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ssl
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.load_cert_chain('ssl.cert', 'ssl.key')
|
||||
run_simple('localhost', 4000, application, ssl_context=ctx)
|
||||
|
||||
|
||||
.. versionchanged 0.10:: ``OpenSSL`` contexts are not supported anymore.
|
||||
|
||||
Generating Certificates
|
||||
```````````````````````
|
||||
|
||||
A key and certificate can be created in advance using the openssl tool
|
||||
instead of the :func:`make_ssl_devcert`. This requires that you have
|
||||
the `openssl` command installed on your system::
|
||||
|
||||
$ openssl genrsa 1024 > ssl.key
|
||||
$ openssl req -new -x509 -nodes -sha1 -days 365 -key ssl.key > ssl.cert
|
||||
|
||||
Adhoc Certificates
|
||||
``````````````````
|
||||
|
||||
The easiest way to enable SSL is to start the server in adhoc-mode. In
|
||||
that case Werkzeug will generate an SSL certificate for you::
|
||||
|
||||
run_simple('localhost', 4000, application,
|
||||
ssl_context='adhoc')
|
||||
|
||||
The downside of this of course is that you will have to acknowledge the
|
||||
certificate each time the server is reloaded. Adhoc certificates are
|
||||
discouraged because modern browsers do a bad job at supporting them for
|
||||
security reasons.
|
||||
|
||||
This feature requires the cryptography library to be installed.
|
||||
|
||||
|
||||
Unix Sockets
|
||||
------------
|
||||
|
||||
The dev server can bind to a Unix socket instead of a TCP socket.
|
||||
:func:`run_simple` will bind to a Unix socket if the ``hostname``
|
||||
parameter starts with ``'unix://'``. ::
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple('unix://example.sock', 0, app)
|
|
@ -0,0 +1,44 @@
|
|||
===============
|
||||
Important Terms
|
||||
===============
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
This page covers important terms used in the documentation and Werkzeug
|
||||
itself.
|
||||
|
||||
|
||||
WSGI
|
||||
----
|
||||
|
||||
WSGI a specification for Python web applications Werkzeug follows. It was
|
||||
specified in the :pep:`3333` and is widely supported. Unlike previous solutions
|
||||
it guarantees that web applications, servers and utilities can work together.
|
||||
|
||||
Response Object
|
||||
---------------
|
||||
|
||||
For Werkzeug, a response object is an object that works like a WSGI
|
||||
application but does not do any request processing. Usually you have a view
|
||||
function or controller method that processes the request and assembles a
|
||||
response object.
|
||||
|
||||
A response object is *not* necessarily the :class:`Response` class or a
|
||||
subclass thereof.
|
||||
|
||||
For example Pylons/webob provide a very similar response class that can
|
||||
be used as well (:class:`webob.Response`).
|
||||
|
||||
View Function
|
||||
-------------
|
||||
|
||||
Often people speak of MVC (Model, View, Controller) when developing web
|
||||
applications. However, the Django framework coined MTV (Model, Template,
|
||||
View) which basically means the same but reduces the concept to the data
|
||||
model, a function that processes data from the request and the database and
|
||||
renders a template.
|
||||
|
||||
Werkzeug itself does not tell you how you should develop applications, but the
|
||||
documentation often speaks of view functions that work roughly the same. The
|
||||
idea of a view function is that it's called with a request object (and
|
||||
optionally some parameters from an URL rule) and returns a response object.
|
|
@ -0,0 +1,111 @@
|
|||
.. module:: werkzeug.test
|
||||
|
||||
Testing WSGI Applications
|
||||
=========================
|
||||
|
||||
|
||||
Test Client
|
||||
-----------
|
||||
|
||||
Werkzeug provides a :class:`Client` to simulate requests to a WSGI
|
||||
application without starting a server. The client has methods for making
|
||||
different types of requests, as well as managing cookies across
|
||||
requests.
|
||||
|
||||
>>> from werkzeug.test import Client
|
||||
>>> from werkzeug.testapp import test_app
|
||||
>>> c = Client(test_app)
|
||||
>>> response = c.get("/")
|
||||
>>> response.status_code
|
||||
200
|
||||
>>> resp.headers
|
||||
Headers([('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', '6658')])
|
||||
>>> response.get_data(as_text=True)
|
||||
'<!doctype html>...'
|
||||
|
||||
The client's request methods return instances of :class:`TestResponse`.
|
||||
This provides extra attributes and methods on top of
|
||||
:class:`~werkzeug.wrappers.Response` that are useful for testing.
|
||||
|
||||
|
||||
Request Body
|
||||
------------
|
||||
|
||||
By passing a dict to ``data``, the client will construct a request body
|
||||
with file and form data. It will set the content type to
|
||||
``application/x-www-form-urlencoded`` if there are no files, or
|
||||
``multipart/form-data`` there are.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import io
|
||||
|
||||
response = client.post(data={
|
||||
"name": "test",
|
||||
"file": (BytesIO("file contents".encode("utf8")), "test.txt")
|
||||
})
|
||||
|
||||
Pass a string, bytes, or file-like object to ``data`` to use that as the
|
||||
raw request body. In that case, you should set the content type
|
||||
appropriately. For example, to post YAML:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = client.post(
|
||||
data="a: value\nb: 1\n", content_type="application/yaml"
|
||||
)
|
||||
|
||||
A shortcut when testing JSON APIs is to pass a dict to ``json`` instead
|
||||
of using ``data``. This will automatically call ``json.dumps()`` and
|
||||
set the content type to ``application/json``. Additionally, if the
|
||||
app returns JSON, ``response.json`` will automatically call
|
||||
``json.loads()``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = client.post("/api", json={"a": "value", "b": 1})
|
||||
obj = response.json()
|
||||
|
||||
|
||||
Environment Builder
|
||||
-------------------
|
||||
|
||||
:class:`EnvironBuilder` is used to construct a WSGI environ dict. The
|
||||
test client uses this internally to prepare its requests. The arguments
|
||||
passed to the client request methods are the same as the builder.
|
||||
|
||||
Sometimes, it can be useful to construct a WSGI environment manually.
|
||||
An environ builder or dict can be passed to the test client request
|
||||
methods in place of other arguments to use a custom environ.
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
from werkzeug.test import EnvironBuilder
|
||||
builder = EnvironBuilder(...)
|
||||
# build an environ dict
|
||||
environ = builder.get_environ()
|
||||
# build an environ dict wrapped in a request
|
||||
request = builder.get_request()
|
||||
|
||||
The test client responses make this available through
|
||||
:attr:`TestResponse.request` and ``response.request.environ``.
|
||||
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. autoclass:: Client
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autoclass:: TestResponse
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autoclass:: EnvironBuilder
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autofunction:: create_environ
|
||||
|
||||
.. autofunction:: run_wsgi_app
|
|
@ -0,0 +1,479 @@
|
|||
=================
|
||||
Werkzeug Tutorial
|
||||
=================
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
Welcome to the Werkzeug tutorial in which we will create a `TinyURL`_ clone
|
||||
that stores URLs in a redis instance. The libraries we will use for this
|
||||
applications are `Jinja`_ 2 for the templates, `redis`_ for the database
|
||||
layer and, of course, Werkzeug for the WSGI layer.
|
||||
|
||||
You can use `pip` to install the required libraries::
|
||||
|
||||
pip install Jinja2 redis Werkzeug
|
||||
|
||||
Also make sure to have a redis server running on your local machine. If
|
||||
you are on OS X, you can use `brew` to install it::
|
||||
|
||||
brew install redis
|
||||
|
||||
If you are on Ubuntu or Debian, you can use apt-get::
|
||||
|
||||
sudo apt-get install redis-server
|
||||
|
||||
Redis was developed for UNIX systems and was never really designed to
|
||||
work on Windows. For development purposes, the unofficial ports however
|
||||
work well enough. You can get them from `github
|
||||
<https://github.com/dmajkic/redis/downloads>`_.
|
||||
|
||||
Introducing Shortly
|
||||
-------------------
|
||||
|
||||
In this tutorial, we will together create a simple URL shortener service
|
||||
with Werkzeug. Please keep in mind that Werkzeug is not a framework, it's
|
||||
a library with utilities to create your own framework or application and
|
||||
as such is very flexible. The approach we use here is just one of many you
|
||||
can use.
|
||||
|
||||
As data store, we will use `redis`_ here instead of a relational database
|
||||
to keep this simple and because that's the kind of job that `redis`_
|
||||
excels at.
|
||||
|
||||
The final result will look something like this:
|
||||
|
||||
.. image:: _static/shortly.png
|
||||
:alt: a screenshot of shortly
|
||||
|
||||
.. _TinyURL: https://tinyurl.com/
|
||||
.. _Jinja: http://jinja.pocoo.org/
|
||||
.. _redis: https://redis.io/
|
||||
|
||||
Step 0: A Basic WSGI Introduction
|
||||
---------------------------------
|
||||
|
||||
Werkzeug is a utility library for WSGI. WSGI itself is a protocol or
|
||||
convention that ensures that your web application can speak with the
|
||||
webserver and more importantly that web applications work nicely together.
|
||||
|
||||
A basic “Hello World” application in WSGI without the help of Werkzeug
|
||||
looks like this::
|
||||
|
||||
def application(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return ['Hello World!'.encode('utf-8')]
|
||||
|
||||
A WSGI application is something you can call and pass an environ dict
|
||||
and a ``start_response`` callable. The environ contains all incoming
|
||||
information, the ``start_response`` function can be used to indicate the
|
||||
start of the response. With Werkzeug you don't have to deal directly with
|
||||
either as request and response objects are provided to work with them.
|
||||
|
||||
The request data takes the environ object and allows you to access the
|
||||
data from that environ in a nice manner. The response object is a WSGI
|
||||
application in itself and provides a much nicer way to create responses.
|
||||
|
||||
Here is how you would write that application with response objects::
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
def application(environ, start_response):
|
||||
response = Response('Hello World!', mimetype='text/plain')
|
||||
return response(environ, start_response)
|
||||
|
||||
And here an expanded version that looks at the query string in the URL
|
||||
(more importantly at the `name` parameter in the URL to substitute “World”
|
||||
against another word)::
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
def application(environ, start_response):
|
||||
request = Request(environ)
|
||||
text = f"Hello {request.args.get('name', 'World')}!"
|
||||
response = Response(text, mimetype='text/plain')
|
||||
return response(environ, start_response)
|
||||
|
||||
And that's all you need to know about WSGI.
|
||||
|
||||
|
||||
Step 1: Creating the Folders
|
||||
----------------------------
|
||||
|
||||
Before we get started, let’s create the folders needed for this application::
|
||||
|
||||
/shortly
|
||||
/static
|
||||
/templates
|
||||
|
||||
The shortly folder is not a python package, but just something where we
|
||||
drop our files. Directly into this folder we will then put our main
|
||||
module in the following steps. The files inside the static folder are
|
||||
available to users of the application via HTTP. This is the place where
|
||||
CSS and JavaScript files go. Inside the templates folder we will make
|
||||
Jinja2 look for templates. The templates you create later in the tutorial
|
||||
will go in this directory.
|
||||
|
||||
Step 2: The Base Structure
|
||||
--------------------------
|
||||
|
||||
Now let's get right into it and create a module for our application. Let's
|
||||
create a file called `shortly.py` in the `shortly` folder. At first we
|
||||
will need a bunch of imports. I will pull in all the imports here, even
|
||||
if they are not used right away, to keep it from being confusing::
|
||||
|
||||
import os
|
||||
import redis
|
||||
from werkzeug.urls import url_parse
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
from werkzeug.utils import redirect
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
Then we can create the basic structure for our application and a function
|
||||
to create a new instance of it, optionally with a piece of WSGI middleware
|
||||
that exports all the files on the `static` folder on the web::
|
||||
|
||||
class Shortly(object):
|
||||
|
||||
def __init__(self, config):
|
||||
self.redis = redis.Redis(
|
||||
config['redis_host'], config['redis_port'], decode_responses=True
|
||||
)
|
||||
|
||||
def dispatch_request(self, request):
|
||||
return Response('Hello World!')
|
||||
|
||||
def wsgi_app(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
response = self.dispatch_request(request)
|
||||
return response(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
|
||||
|
||||
def create_app(redis_host='localhost', redis_port=6379, with_static=True):
|
||||
app = Shortly({
|
||||
'redis_host': redis_host,
|
||||
'redis_port': redis_port
|
||||
})
|
||||
if with_static:
|
||||
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
|
||||
'/static': os.path.join(os.path.dirname(__file__), 'static')
|
||||
})
|
||||
return app
|
||||
|
||||
Lastly we can add a piece of code that will start a local development
|
||||
server with automatic code reloading and a debugger::
|
||||
|
||||
if __name__ == '__main__':
|
||||
from werkzeug.serving import run_simple
|
||||
app = create_app()
|
||||
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
|
||||
|
||||
The basic idea here is that our ``Shortly`` class is an actual WSGI
|
||||
application. The ``__call__`` method directly dispatches to ``wsgi_app``.
|
||||
This is done so that we can wrap ``wsgi_app`` to apply middlewares like we
|
||||
do in the ``create_app`` function. The actual ``wsgi_app`` method then
|
||||
creates a :class:`Request` object and calls the ``dispatch_request``
|
||||
method which then has to return a :class:`Response` object which is then
|
||||
evaluated as WSGI application again. As you can see: turtles all the way
|
||||
down. Both the ``Shortly`` class we create, as well as any request object
|
||||
in Werkzeug implements the WSGI interface. As a result of that you could
|
||||
even return another WSGI application from the ``dispatch_request`` method.
|
||||
|
||||
The ``create_app`` factory function can be used to create a new instance
|
||||
of our application. Not only will it pass some parameters as
|
||||
configuration to the application but also optionally add a WSGI middleware
|
||||
that exports static files. This way we have access to the files from the
|
||||
static folder even when we are not configuring our server to provide them
|
||||
which is very helpful for development.
|
||||
|
||||
Intermezzo: Running the Application
|
||||
-----------------------------------
|
||||
|
||||
Now you should be able to execute the file with `python` and see a server
|
||||
on your local machine::
|
||||
|
||||
$ python shortly.py
|
||||
* Running on http://127.0.0.1:5000/
|
||||
* Restarting with reloader: stat() polling
|
||||
|
||||
It also tells you that the reloader is active. It will use various
|
||||
techniques to figure out if any file changed on the disk and then
|
||||
automatically restart.
|
||||
|
||||
Just go to the URL and you should see “Hello World!”.
|
||||
|
||||
Step 3: The Environment
|
||||
-----------------------
|
||||
|
||||
Now that we have the basic application class, we can make the constructor
|
||||
do something useful and provide a few helpers on there that can come in
|
||||
handy. We will need to be able to render templates and connect to redis,
|
||||
so let's extend the class a bit::
|
||||
|
||||
def __init__(self, config):
|
||||
self.redis = redis.Redis(config['redis_host'], config['redis_port'])
|
||||
template_path = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(template_path),
|
||||
autoescape=True)
|
||||
|
||||
def render_template(self, template_name, **context):
|
||||
t = self.jinja_env.get_template(template_name)
|
||||
return Response(t.render(context), mimetype='text/html')
|
||||
|
||||
Step 4: The Routing
|
||||
-------------------
|
||||
|
||||
Next up is routing. Routing is the process of matching and parsing the URL to
|
||||
something we can use. Werkzeug provides a flexible integrated routing
|
||||
system which we can use for that. The way it works is that you create a
|
||||
:class:`~werkzeug.routing.Map` instance and add a bunch of
|
||||
:class:`~werkzeug.routing.Rule` objects. Each rule has a pattern it will
|
||||
try to match the URL against and an “endpoint”. The endpoint is typically
|
||||
a string and can be used to uniquely identify the URL. We could also use
|
||||
this to automatically reverse the URL, but that's not what we will do in this
|
||||
tutorial.
|
||||
|
||||
Just put this into the constructor::
|
||||
|
||||
self.url_map = Map([
|
||||
Rule('/', endpoint='new_url'),
|
||||
Rule('/<short_id>', endpoint='follow_short_link'),
|
||||
Rule('/<short_id>+', endpoint='short_link_details')
|
||||
])
|
||||
|
||||
Here we create a URL map with three rules. ``/`` for the root of the URL
|
||||
space where we will just dispatch to a function that implements the logic
|
||||
to create a new URL. And then one that follows the short link to the
|
||||
target URL and another one with the same rule but a plus (``+``) at the
|
||||
end to show the link details.
|
||||
|
||||
So how do we find our way from the endpoint to a function? That's up to you.
|
||||
The way we will do it in this tutorial is by calling the method ``on_``
|
||||
+ endpoint on the class itself. Here is how this works::
|
||||
|
||||
def dispatch_request(self, request):
|
||||
adapter = self.url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
return getattr(self, f'on_{endpoint}')(request, **values)
|
||||
except HTTPException as e:
|
||||
return e
|
||||
|
||||
We bind the URL map to the current environment and get back a
|
||||
:class:`~werkzeug.routing.URLAdapter`. The adapter can be used to match
|
||||
the request but also to reverse URLs. The match method will return the
|
||||
endpoint and a dictionary of values in the URL. For instance the rule for
|
||||
``follow_short_link`` has a variable part called ``short_id``. When we go
|
||||
to ``http://localhost:5000/foo`` we will get the following values back::
|
||||
|
||||
endpoint = 'follow_short_link'
|
||||
values = {'short_id': 'foo'}
|
||||
|
||||
If it does not match anything, it will raise a
|
||||
:exc:`~werkzeug.exceptions.NotFound` exception, which is an
|
||||
:exc:`~werkzeug.exceptions.HTTPException`. All HTTP exceptions are also
|
||||
WSGI applications by themselves which render a default error page. So we
|
||||
just catch all of them down and return the error itself.
|
||||
|
||||
If all works well, we call the function ``on_`` + endpoint and pass it the
|
||||
request as argument as well as all the URL arguments as keyword arguments
|
||||
and return the response object that method returns.
|
||||
|
||||
Step 5: The First View
|
||||
----------------------
|
||||
|
||||
Let's start with the first view: the one for new URLs::
|
||||
|
||||
def on_new_url(self, request):
|
||||
error = None
|
||||
url = ''
|
||||
if request.method == 'POST':
|
||||
url = request.form['url']
|
||||
if not is_valid_url(url):
|
||||
error = 'Please enter a valid URL'
|
||||
else:
|
||||
short_id = self.insert_url(url)
|
||||
return redirect(f"/{short_id}+")
|
||||
return self.render_template('new_url.html', error=error, url=url)
|
||||
|
||||
This logic should be easy to understand. Basically we are checking that
|
||||
the request method is POST, in which case we validate the URL and add a
|
||||
new entry to the database, then redirect to the detail page. This means
|
||||
we need to write a function and a helper method. For URL validation this
|
||||
is good enough::
|
||||
|
||||
def is_valid_url(url):
|
||||
parts = url_parse(url)
|
||||
return parts.scheme in ('http', 'https')
|
||||
|
||||
For inserting the URL, all we need is this little method on our class::
|
||||
|
||||
def insert_url(self, url):
|
||||
short_id = self.redis.get(f'reverse-url:{url}')
|
||||
if short_id is not None:
|
||||
return short_id
|
||||
url_num = self.redis.incr('last-url-id')
|
||||
short_id = base36_encode(url_num)
|
||||
self.redis.set(f'url-target:{short_id}', url)
|
||||
self.redis.set(f'reverse-url:{url}', short_id)
|
||||
return short_id
|
||||
|
||||
``reverse-url:`` + the URL will store the short id. If the URL was
|
||||
already submitted this won't be None and we can just return that value
|
||||
which will be the short ID. Otherwise we increment the ``last-url-id``
|
||||
key and convert it to base36. Then we store the link and the reverse
|
||||
entry in redis. And here the function to convert to base 36::
|
||||
|
||||
def base36_encode(number):
|
||||
assert number >= 0, 'positive integer required'
|
||||
if number == 0:
|
||||
return '0'
|
||||
base36 = []
|
||||
while number != 0:
|
||||
number, i = divmod(number, 36)
|
||||
base36.append('0123456789abcdefghijklmnopqrstuvwxyz'[i])
|
||||
return ''.join(reversed(base36))
|
||||
|
||||
So what is missing for this view to work is the template. We will create
|
||||
this later, let's first also write the other views and then do the
|
||||
templates in one go.
|
||||
|
||||
Step 6: Redirect View
|
||||
---------------------
|
||||
|
||||
The redirect view is easy. All it has to do is to look for the link in
|
||||
redis and redirect to it. Additionally we will also increment a counter
|
||||
so that we know how often a link was clicked::
|
||||
|
||||
def on_follow_short_link(self, request, short_id):
|
||||
link_target = self.redis.get(f'url-target:{short_id}')
|
||||
if link_target is None:
|
||||
raise NotFound()
|
||||
self.redis.incr(f'click-count:{short_id}')
|
||||
return redirect(link_target)
|
||||
|
||||
In this case we will raise a :exc:`~werkzeug.exceptions.NotFound` exception
|
||||
by hand if the URL does not exist, which will bubble up to the
|
||||
``dispatch_request`` function and be converted into a default 404
|
||||
response.
|
||||
|
||||
Step 7: Detail View
|
||||
-------------------
|
||||
|
||||
The link detail view is very similar, we just render a template
|
||||
again. In addition to looking up the target, we also ask redis for the
|
||||
number of times the link was clicked and let it default to zero if such
|
||||
a key does not yet exist::
|
||||
|
||||
def on_short_link_details(self, request, short_id):
|
||||
link_target = self.redis.get(f'url-target:{short_id}')
|
||||
if link_target is None:
|
||||
raise NotFound()
|
||||
click_count = int(self.redis.get(f'click-count:{short_id}') or 0)
|
||||
return self.render_template('short_link_details.html',
|
||||
link_target=link_target,
|
||||
short_id=short_id,
|
||||
click_count=click_count
|
||||
)
|
||||
|
||||
Please be aware that redis always works with strings, so you have to convert
|
||||
the click count to :class:`int` by hand.
|
||||
|
||||
Step 8: Templates
|
||||
-----------------
|
||||
|
||||
And here are all the templates. Just drop them into the `templates`
|
||||
folder. Jinja2 supports template inheritance, so the first thing we will
|
||||
do is create a layout template with blocks that act as placeholders. We
|
||||
also set up Jinja2 so that it automatically escapes strings with HTML
|
||||
rules, so we don't have to spend time on that ourselves. This prevents
|
||||
XSS attacks and rendering errors.
|
||||
|
||||
*layout.html*:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
<!doctype html>
|
||||
<title>{% block title %}{% endblock %} | shortly</title>
|
||||
<link rel=stylesheet href=/static/style.css type=text/css>
|
||||
<div class=box>
|
||||
<h1><a href=/>shortly</a></h1>
|
||||
<p class=tagline>Shortly is a URL shortener written with Werkzeug
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
*new_url.html*:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Create New Short URL{% endblock %}
|
||||
{% block body %}
|
||||
<h2>Submit URL</h2>
|
||||
<form action="" method=post>
|
||||
{% if error %}
|
||||
<p class=error><strong>Error:</strong> {{ error }}
|
||||
{% endif %}
|
||||
<p>URL:
|
||||
<input type=text name=url value="{{ url }}" class=urlinput>
|
||||
<input type=submit value="Shorten">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
*short_link_details.html*:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Details about /{{ short_id }}{% endblock %}
|
||||
{% block body %}
|
||||
<h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
|
||||
<dl>
|
||||
<dt>Full link
|
||||
<dd class=link><div>{{ link_target }}</div>
|
||||
<dt>Click count:
|
||||
<dd>{{ click_count }}
|
||||
</dl>
|
||||
{% endblock %}
|
||||
|
||||
Step 9: The Style
|
||||
-----------------
|
||||
|
||||
For this to look better than ugly black and white, here a simple
|
||||
stylesheet that goes along:
|
||||
|
||||
*static/style.css*:
|
||||
|
||||
.. sourcecode:: css
|
||||
|
||||
body { background: #E8EFF0; margin: 0; padding: 0; }
|
||||
body, input { font-family: 'Helvetica Neue', Arial,
|
||||
sans-serif; font-weight: 300; font-size: 18px; }
|
||||
.box { width: 500px; margin: 60px auto; padding: 20px;
|
||||
background: white; box-shadow: 0 1px 4px #BED1D4;
|
||||
border-radius: 2px; }
|
||||
a { color: #11557C; }
|
||||
h1, h2 { margin: 0; color: #11557C; }
|
||||
h1 a { text-decoration: none; }
|
||||
h2 { font-weight: normal; font-size: 24px; }
|
||||
.tagline { color: #888; font-style: italic; margin: 0 0 20px 0; }
|
||||
.link div { overflow: auto; font-size: 0.8em; white-space: pre;
|
||||
padding: 4px 10px; margin: 5px 0; background: #E5EAF1; }
|
||||
dt { font-weight: normal; }
|
||||
.error { background: #E8EFF0; padding: 3px 8px; color: #11557C;
|
||||
font-size: 0.9em; border-radius: 2px; }
|
||||
.urlinput { width: 300px; }
|
||||
|
||||
Bonus: Refinements
|
||||
------------------
|
||||
|
||||
Look at the implementation in the example dictionary in the Werkzeug
|
||||
repository to see a version of this tutorial with some small refinements
|
||||
such as a custom 404 page.
|
||||
|
||||
- `shortly in the example folder <https://github.com/pallets/werkzeug/tree/main/examples/shortly>`_
|
|
@ -0,0 +1,76 @@
|
|||
Unicode
|
||||
=======
|
||||
|
||||
.. currentmodule:: werkzeug
|
||||
|
||||
Werkzeug uses strings internally everwhere text data is assumed, even if
|
||||
the HTTP standard is not Unicode aware. Basically all incoming data is
|
||||
decoded from the charset (UTF-8 by default) so that you don't work with
|
||||
bytes directly. Outgoing data is encoded into the target charset.
|
||||
|
||||
|
||||
Unicode in Python
|
||||
-----------------
|
||||
|
||||
Imagine you have the German Umlaut ``ö``. In ASCII you cannot represent
|
||||
that character, but in the ``latin-1`` and ``utf-8`` character sets you
|
||||
can represent it, but they look different when encoded:
|
||||
|
||||
>>> "ö".encode("latin1")
|
||||
b'\xf6'
|
||||
>>> "ö".encode("utf-8")
|
||||
b'\xc3\xb6'
|
||||
|
||||
An ``ö`` looks different depending on the encoding which makes it hard
|
||||
to work with it as bytes. Instead, Python treats strings as Unicode text
|
||||
and stores the information ``LATIN SMALL LETTER O WITH DIAERESIS``
|
||||
instead of the bytes for ``ö`` in a specific encoding. The length of a
|
||||
string with 1 character will be 1, where the length of the bytes might
|
||||
be some other value.
|
||||
|
||||
|
||||
Unicode in HTTP
|
||||
---------------
|
||||
|
||||
However, the HTTP spec was written in a time where ASCII bytes were the
|
||||
common way data was represented. To work around this for the modern
|
||||
web, Werkzeug decodes and encodes incoming and outgoing data
|
||||
automatically. Data sent from the browser to the web application is
|
||||
decoded from UTF-8 bytes into a string. Data sent from the application
|
||||
back to the browser is encoded back to UTF-8.
|
||||
|
||||
|
||||
Error Handling
|
||||
--------------
|
||||
|
||||
Functions that do internal encoding or decoding accept an ``errors``
|
||||
keyword argument that is passed to :meth:`str.decode` and
|
||||
:meth:`str.encode`. The default is ``'replace'`` so that errors are easy
|
||||
to spot. It might be useful to set it to ``'strict'`` in order to catch
|
||||
the error and report the bad data to the client.
|
||||
|
||||
|
||||
Request and Response Objects
|
||||
----------------------------
|
||||
|
||||
In most cases, you should stick with Werkzeug's default encoding of
|
||||
UTF-8. If you have a specific reason to, you can subclass
|
||||
:class:`wrappers.Request` and :class:`wrappers.Response` to change the
|
||||
encoding and error handling.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from werkzeug.wrappers.request import Request
|
||||
from werkzeug.wrappers.response import Response
|
||||
|
||||
class Latin1Request(Request):
|
||||
charset = "latin1"
|
||||
encoding_errors = "strict"
|
||||
|
||||
class Latin1Response(Response):
|
||||
charset = "latin1"
|
||||
|
||||
The error handling can only be changed for the request. Werkzeug will
|
||||
always raise errors when encoding to bytes in the response. It's your
|
||||
responsibility to not create data that is not present in the target
|
||||
charset. This is not an issue for UTF-8.
|
|
@ -0,0 +1,6 @@
|
|||
===========
|
||||
URL Helpers
|
||||
===========
|
||||
|
||||
.. automodule:: werkzeug.urls
|
||||
:members:
|
|
@ -0,0 +1,74 @@
|
|||
=========
|
||||
Utilities
|
||||
=========
|
||||
|
||||
Various utility functions shipped with Werkzeug.
|
||||
|
||||
.. module:: werkzeug.utils
|
||||
|
||||
|
||||
General Helpers
|
||||
===============
|
||||
|
||||
.. autoclass:: cached_property
|
||||
:members:
|
||||
|
||||
.. autoclass:: environ_property
|
||||
|
||||
.. autoclass:: header_property
|
||||
|
||||
.. autofunction:: redirect
|
||||
|
||||
.. autofunction:: append_slash_redirect
|
||||
|
||||
.. autofunction:: send_file
|
||||
|
||||
.. autofunction:: import_string
|
||||
|
||||
.. autofunction:: find_modules
|
||||
|
||||
.. autofunction:: secure_filename
|
||||
|
||||
|
||||
URL Helpers
|
||||
===========
|
||||
|
||||
Please refer to :doc:`urls`.
|
||||
|
||||
|
||||
User Agent API
|
||||
==============
|
||||
|
||||
.. module:: werkzeug.user_agent
|
||||
|
||||
.. autoclass:: UserAgent
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
Security Helpers
|
||||
================
|
||||
|
||||
.. module:: werkzeug.security
|
||||
|
||||
.. autofunction:: generate_password_hash
|
||||
|
||||
.. autofunction:: check_password_hash
|
||||
|
||||
.. autofunction:: safe_join
|
||||
|
||||
|
||||
Logging
|
||||
=======
|
||||
|
||||
Werkzeug uses standard Python :mod:`logging`. The logger is named
|
||||
``"werkzeug"``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("werkzeug")
|
||||
|
||||
If the logger level is not set, it will be set to :data:`~logging.INFO`
|
||||
on first use. If there is no handler for that level, a
|
||||
:class:`~logging.StreamHandler` is added.
|
|
@ -0,0 +1,92 @@
|
|||
==========================
|
||||
Request / Response Objects
|
||||
==========================
|
||||
|
||||
.. module:: werkzeug.wrappers
|
||||
|
||||
The request and response objects wrap the WSGI environment or the return
|
||||
value from a WSGI application so that it is another WSGI application
|
||||
(wraps a whole application).
|
||||
|
||||
How they Work
|
||||
=============
|
||||
|
||||
Your WSGI application is always passed two arguments. The WSGI "environment"
|
||||
and the WSGI `start_response` function that is used to start the response
|
||||
phase. The :class:`Request` class wraps the `environ` for easier access to
|
||||
request variables (form data, request headers etc.).
|
||||
|
||||
The :class:`Response` on the other hand is a standard WSGI application that
|
||||
you can create. The simple hello world in Werkzeug looks like this::
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
application = Response('Hello World!')
|
||||
|
||||
To make it more useful you can replace it with a function and do some
|
||||
processing::
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
def application(environ, start_response):
|
||||
request = Request(environ)
|
||||
response = Response(f"Hello {request.args.get('name', 'World!')}!")
|
||||
return response(environ, start_response)
|
||||
|
||||
Because this is a very common task the :class:`~Request` object provides
|
||||
a helper for that. The above code can be rewritten like this::
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
@Request.application
|
||||
def application(request):
|
||||
return Response(f"Hello {request.args.get('name', 'World!')}!")
|
||||
|
||||
The `application` is still a valid WSGI application that accepts the
|
||||
environment and `start_response` callable.
|
||||
|
||||
|
||||
Mutability and Reusability of Wrappers
|
||||
======================================
|
||||
|
||||
The implementation of the Werkzeug request and response objects are trying
|
||||
to guard you from common pitfalls by disallowing certain things as much as
|
||||
possible. This serves two purposes: high performance and avoiding of
|
||||
pitfalls.
|
||||
|
||||
For the request object the following rules apply:
|
||||
|
||||
1. The request object is immutable. Modifications are not supported by
|
||||
default, you may however replace the immutable attributes with mutable
|
||||
attributes if you need to modify it.
|
||||
2. The request object may be shared in the same thread, but is not thread
|
||||
safe itself. If you need to access it from multiple threads, use
|
||||
locks around calls.
|
||||
3. It's not possible to pickle the request object.
|
||||
|
||||
For the response object the following rules apply:
|
||||
|
||||
1. The response object is mutable
|
||||
2. The response object can be pickled or copied after `freeze()` was
|
||||
called.
|
||||
3. Since Werkzeug 0.6 it's safe to use the same response object for
|
||||
multiple WSGI responses.
|
||||
4. It's possible to create copies using `copy.deepcopy`.
|
||||
|
||||
|
||||
Wrapper Classes
|
||||
===============
|
||||
|
||||
.. autoclass:: Request
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. automethod:: _get_file_stream
|
||||
|
||||
|
||||
.. autoclass:: Response
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. automethod:: __call__
|
||||
|
||||
.. automethod:: _ensure_sequence
|
|
@ -0,0 +1,115 @@
|
|||
WSGI Helpers
|
||||
============
|
||||
|
||||
.. module:: werkzeug.wsgi
|
||||
|
||||
The following classes and functions are designed to make working with
|
||||
the WSGI specification easier or operate on the WSGI layer. All the
|
||||
functionality from this module is available on the high-level
|
||||
:doc:`/wrappers`.
|
||||
|
||||
|
||||
Iterator / Stream Helpers
|
||||
-------------------------
|
||||
|
||||
These classes and functions simplify working with the WSGI application
|
||||
iterator and the input stream.
|
||||
|
||||
.. autoclass:: ClosingIterator
|
||||
|
||||
.. autoclass:: FileWrapper
|
||||
|
||||
.. autoclass:: LimitedStream
|
||||
:members:
|
||||
|
||||
.. autofunction:: make_line_iter
|
||||
|
||||
.. autofunction:: make_chunk_iter
|
||||
|
||||
.. autofunction:: wrap_file
|
||||
|
||||
|
||||
Environ Helpers
|
||||
---------------
|
||||
|
||||
These functions operate on the WSGI environment. They extract useful
|
||||
information or perform common manipulations:
|
||||
|
||||
.. autofunction:: get_host
|
||||
|
||||
.. autofunction:: get_content_length
|
||||
|
||||
.. autofunction:: get_input_stream
|
||||
|
||||
.. autofunction:: get_current_url
|
||||
|
||||
.. autofunction:: get_query_string
|
||||
|
||||
.. autofunction:: get_script_name
|
||||
|
||||
.. autofunction:: get_path_info
|
||||
|
||||
.. autofunction:: pop_path_info
|
||||
|
||||
.. autofunction:: peek_path_info
|
||||
|
||||
.. autofunction:: extract_path_info
|
||||
|
||||
.. autofunction:: host_is_trusted
|
||||
|
||||
|
||||
Convenience Helpers
|
||||
-------------------
|
||||
|
||||
.. autofunction:: responder
|
||||
|
||||
.. autofunction:: werkzeug.testapp.test_app
|
||||
|
||||
|
||||
Bytes, Strings, and Encodings
|
||||
-----------------------------
|
||||
|
||||
The values in HTTP requests come in as bytes representing (or encoded
|
||||
to) ASCII. The WSGI specification (:pep:`3333`) decided to always use
|
||||
the ``str`` type to represent values. To accomplish this, the raw bytes
|
||||
are decoded using the ISO-8859-1 charset to produce a string.
|
||||
|
||||
Strings in the WSGI environment are restricted to ISO-8859-1 code
|
||||
points. If a string read from the environment might contain characters
|
||||
outside that charset, it must first be decoded to bytes as ISO-8859-1,
|
||||
then encoded to a string using the proper charset (typically UTF-8). The
|
||||
reverse is done when writing to the environ. This is known as the "WSGI
|
||||
encoding dance".
|
||||
|
||||
Werkzeug provides functions to deal with this automatically so that you
|
||||
don't need to be aware of the inner workings. Use the functions on this
|
||||
page as well as :func:`~werkzeug.datastructures.EnvironHeaders` to read
|
||||
data out of the WSGI environment.
|
||||
|
||||
Applications should avoid manually creating or modifying a WSGI
|
||||
environment unless they take care of the proper encoding or decoding
|
||||
step. All high level interfaces in Werkzeug will apply the encoding and
|
||||
decoding as necessary.
|
||||
|
||||
|
||||
Raw Request URI and Path Encoding
|
||||
---------------------------------
|
||||
|
||||
The ``PATH_INFO`` in the environ is the path value after
|
||||
percent-decoding. For example, the raw path ``/hello%2fworld`` would
|
||||
show up from the WSGI server to Werkzeug as ``/hello/world``. This loses
|
||||
the information that the slash was a raw character as opposed to a path
|
||||
separator.
|
||||
|
||||
The WSGI specification (:pep:`3333`) does not provide a way to get the
|
||||
original value, so it is impossible to route some types of data in the
|
||||
path. The most compatible way to work around this is to send problematic
|
||||
data in the query string instead of the path.
|
||||
|
||||
However, many WSGI servers add a non-standard environ key with the raw
|
||||
path. To match this behavior, Werkzeug's test client and development
|
||||
server will add the raw value to both the ``REQUEST_URI`` and
|
||||
``RAW_URI`` keys. If you want to route based on this value, you can use
|
||||
middleware to replace ``PATH_INFO`` in the environ before it reaches the
|
||||
application. However, keep in mind that these keys are non-standard and
|
||||
not guaranteed to be present.
|
|
@ -0,0 +1,113 @@
|
|||
=================
|
||||
Werkzeug Examples
|
||||
=================
|
||||
|
||||
This directory contains various example applications and example code of
|
||||
Werkzeug powered applications.
|
||||
|
||||
Beside the proof of concept applications and code snippets in the partial
|
||||
folder they all have external dependencies for template engines or database
|
||||
adapters (SQLAlchemy only so far). Also, every application has click as
|
||||
external dependency, used to create the command line interface.
|
||||
|
||||
|
||||
Full Example Applications
|
||||
=========================
|
||||
|
||||
The following example applications are application types you would actually
|
||||
find in real life :-)
|
||||
|
||||
|
||||
`simplewiki`
|
||||
|
||||
A simple Wiki implementation.
|
||||
|
||||
Requirements:
|
||||
|
||||
- SQLAlchemy
|
||||
- Creoleparser >= 0.7
|
||||
- genshi
|
||||
|
||||
You can obtain all packages in the Cheeseshop via easy_install. You have
|
||||
to have at least version 0.7 of Creoleparser.
|
||||
|
||||
Usage::
|
||||
|
||||
./manage-simplewiki.py initdb
|
||||
./manage-simplewiki.py runserver
|
||||
|
||||
Or of course you can just use the application object
|
||||
(`simplewiki.SimpleWiki`) and hook that into your favourite WSGI gateway.
|
||||
The constructor of the application object takes a single argument which is
|
||||
the SQLAlchemy URI for the database.
|
||||
|
||||
The management script for the devserver looks up the an environment var
|
||||
called `SIMPLEWIKI_DATABASE_URI` and uses that for the database URI. If
|
||||
no such variable is provided "sqlite:////tmp/simplewiki.db" is assumed.
|
||||
|
||||
`plnt`
|
||||
|
||||
A planet called plnt, pronounce plant.
|
||||
|
||||
Requirements:
|
||||
|
||||
- SQLAlchemy
|
||||
- Jinja2
|
||||
- feedparser
|
||||
|
||||
You can obtain all packages in the Cheeseshop via easy_install.
|
||||
|
||||
Usage::
|
||||
|
||||
./manage-plnt.py initdb
|
||||
./manage-plnt.py sync
|
||||
./manage-plnt.py runserver
|
||||
|
||||
The WSGI application is called `plnt.Plnt` which, like the simple wiki,
|
||||
accepts a database URI as first argument. The environment variable for
|
||||
the database key is called `PLNT_DATABASE_URI` and the default is
|
||||
"sqlite:////tmp/plnt.db".
|
||||
|
||||
Per default a few python related blogs are added to the database, you
|
||||
can add more in a python shell by playing with the `Blog` model.
|
||||
|
||||
`shorty`
|
||||
|
||||
A tinyurl clone for the Werkzeug tutorial.
|
||||
|
||||
Requirements:
|
||||
|
||||
- SQLAlchemy
|
||||
- Jinja2
|
||||
|
||||
You can obtain all packages in the Cheeseshop via easy_install.
|
||||
|
||||
Usage::
|
||||
|
||||
./manage-shorty.py initdb
|
||||
./manage-shorty.py runserver
|
||||
|
||||
The WSGI application is called `shorty.application.Shorty` which, like the
|
||||
simple wiki, accepts a database URI as first argument.
|
||||
|
||||
The source code of the application is explained in detail in the Werkzeug
|
||||
tutorial.
|
||||
|
||||
`couchy`
|
||||
|
||||
Like shorty, but implemented using CouchDB.
|
||||
|
||||
Requirements :
|
||||
|
||||
- werkzeug : http://werkzeug.pocoo.org
|
||||
- jinja : http://jinja.pocoo.org
|
||||
- couchdb 0.72 & above : https://couchdb.apache.org/
|
||||
|
||||
`cupoftee`
|
||||
|
||||
A `Teeworlds <https://www.teeworlds.com/>`_ server browser. This application
|
||||
works best in a non forking environment and won't work for CGI.
|
||||
|
||||
Usage::
|
||||
|
||||
./manage-cupoftee.py runserver
|
|
@ -0,0 +1 @@
|
|||
from .application import make_app
|
|
@ -0,0 +1,77 @@
|
|||
"""This module provides the WSGI application.
|
||||
|
||||
The WSGI middlewares are applied in the `make_app` factory function that
|
||||
automatically wraps the application within the require middlewares. Per
|
||||
default only the `SharedDataMiddleware` is applied.
|
||||
"""
|
||||
from os import listdir
|
||||
from os import path
|
||||
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
from werkzeug.routing import Map
|
||||
from werkzeug.routing import RequestRedirect
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from .utils import local_manager
|
||||
from .utils import Request
|
||||
|
||||
|
||||
class CoolMagicApplication:
|
||||
"""
|
||||
The application class. It's passed a directory with configuration values.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
for fn in listdir(path.join(path.dirname(__file__), "views")):
|
||||
if fn.endswith(".py") and fn != "__init__.py":
|
||||
__import__(f"coolmagic.views.{fn[:-3]}")
|
||||
|
||||
from coolmagic.utils import exported_views
|
||||
|
||||
rules = [
|
||||
# url for shared data. this will always be unmatched
|
||||
# because either the middleware or the webserver
|
||||
# handles that request first.
|
||||
Rule("/public/<path:file>", endpoint="shared_data")
|
||||
]
|
||||
self.views = {}
|
||||
for endpoint, (func, rule, extra) in exported_views.items():
|
||||
if rule is not None:
|
||||
rules.append(Rule(rule, endpoint=endpoint, **extra))
|
||||
self.views[endpoint] = func
|
||||
self.url_map = Map(rules)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
urls = self.url_map.bind_to_environ(environ)
|
||||
req = Request(environ, urls)
|
||||
try:
|
||||
endpoint, args = urls.match(req.path)
|
||||
resp = self.views[endpoint](**args)
|
||||
except NotFound:
|
||||
resp = self.views["static.not_found"]()
|
||||
except (HTTPException, RequestRedirect) as e:
|
||||
resp = e
|
||||
return resp(environ, start_response)
|
||||
|
||||
|
||||
def make_app(config=None):
|
||||
"""
|
||||
Factory function that creates a new `CoolmagicApplication`
|
||||
object. Optional WSGI middlewares should be applied here.
|
||||
"""
|
||||
config = config or {}
|
||||
app = CoolMagicApplication(config)
|
||||
|
||||
# static stuff
|
||||
app = SharedDataMiddleware(
|
||||
app, {"/public": path.join(path.dirname(__file__), "public")}
|
||||
)
|
||||
|
||||
# clean up locals
|
||||
app = local_manager.make_middleware(app)
|
||||
|
||||
return app
|
|
@ -0,0 +1,5 @@
|
|||
from .utils import ThreadedRequest
|
||||
|
||||
#: a thread local proxy request object
|
||||
request = ThreadedRequest()
|
||||
del ThreadedRequest
|
|
@ -0,0 +1,10 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
h1, a {
|
||||
color: #a00;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ page_title }} — Cool Magic!</title>
|
||||
<link rel="stylesheet" href="{{ h.url_for('shared_data', file='style.css') }}" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Cool Magic</h1>
|
||||
<h2>{{ page_title }}</h2>
|
||||
{% block page_body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "layout.html" %}
|
||||
{% set page_title = 'About the Magic' %}
|
||||
{% block page_body %}
|
||||
<p>
|
||||
Nothing to see. It's just magic.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ h.url_for('static.index') }}">back to the index</a>
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "layout.html" %}
|
||||
{% set page_title = 'Welcome to the Magic' %}
|
||||
{% block page_body %}
|
||||
<p>
|
||||
Welcome to the magic! This is a bigger example for the
|
||||
Werkzeug toolkit. And it contains a lot of magic.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ h.url_for('static.about') }}">about the implementation</a> or
|
||||
click here if you want to see a <a href="{{ h.url_for('static.broken')
|
||||
}}">broken view</a>.
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% set page_title = 'Missing Magic' %}
|
||||
{% block page_body %}
|
||||
<p>
|
||||
The requested magic really does not exist. Maybe you want
|
||||
to look for it on the <a href="{{ h.url_for('static.index') }}">index</a>.
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,104 @@
|
|||
"""Subclasses of the base request and response objects provided by
|
||||
werkzeug. The subclasses know about their charset and implement some
|
||||
additional functionality like the ability to link to view functions.
|
||||
"""
|
||||
from os.path import dirname
|
||||
from os.path import join
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from werkzeug.local import Local
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.wrappers import Request as BaseRequest
|
||||
from werkzeug.wrappers import Response as BaseResponse
|
||||
|
||||
|
||||
local = Local()
|
||||
local_manager = LocalManager([local])
|
||||
template_env = Environment(
|
||||
loader=FileSystemLoader(join(dirname(__file__), "templates"))
|
||||
)
|
||||
exported_views = {}
|
||||
|
||||
|
||||
def export(string, template=None, **extra):
|
||||
"""
|
||||
Decorator for registering view functions and adding
|
||||
templates to it.
|
||||
"""
|
||||
|
||||
def wrapped(f):
|
||||
endpoint = f"{f.__module__}.{f.__name__}"[16:]
|
||||
if template is not None:
|
||||
old_f = f
|
||||
|
||||
def f(**kwargs):
|
||||
rv = old_f(**kwargs)
|
||||
if not isinstance(rv, Response):
|
||||
rv = TemplateResponse(template, **(rv or {}))
|
||||
return rv
|
||||
|
||||
f.__name__ = old_f.__name__
|
||||
f.__doc__ = old_f.__doc__
|
||||
exported_views[endpoint] = (f, string, extra)
|
||||
return f
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def url_for(endpoint, **values):
|
||||
"""
|
||||
Build a URL
|
||||
"""
|
||||
return local.request.url_adapter.build(endpoint, values)
|
||||
|
||||
|
||||
class Request(BaseRequest):
|
||||
"""
|
||||
The concrete request object used in the WSGI application.
|
||||
It has some helper functions that can be used to build URLs.
|
||||
"""
|
||||
|
||||
charset = "utf-8"
|
||||
|
||||
def __init__(self, environ, url_adapter):
|
||||
super().__init__(environ)
|
||||
self.url_adapter = url_adapter
|
||||
local.request = self
|
||||
|
||||
|
||||
class ThreadedRequest:
|
||||
"""
|
||||
A pseudo request object that always points to the current
|
||||
context active request.
|
||||
"""
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == "__members__":
|
||||
return [x for x in dir(local.request) if not x.startswith("_")]
|
||||
return getattr(local.request, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
return setattr(local.request, name, value)
|
||||
|
||||
|
||||
class Response(BaseResponse):
|
||||
"""
|
||||
The concrete response object for the WSGI application.
|
||||
"""
|
||||
|
||||
charset = "utf-8"
|
||||
default_mimetype = "text/html"
|
||||
|
||||
|
||||
class TemplateResponse(Response):
|
||||
"""
|
||||
Render a template to a response.
|
||||
"""
|
||||
|
||||
def __init__(self, template_name, **values):
|
||||
from coolmagic import helpers
|
||||
|
||||
values.update(request=local.request, h=helpers)
|
||||
template = template_env.get_template(template_name)
|
||||
Response.__init__(self, template.render(values))
|
|
@ -0,0 +1,25 @@
|
|||
from coolmagic.utils import export
|
||||
|
||||
|
||||
@export("/", template="static/index.html")
|
||||
def index():
|
||||
pass
|
||||
|
||||
|
||||
@export("/about", template="static/about.html")
|
||||
def about():
|
||||
pass
|
||||
|
||||
|
||||
@export("/broken")
|
||||
def broken():
|
||||
raise RuntimeError("that's really broken")
|
||||
|
||||
|
||||
@export(None, template="static/not_found.html")
|
||||
def not_found():
|
||||
"""
|
||||
This function is always executed if an url does not
|
||||
match or a `NotFound` exception is raised.
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,7 @@
|
|||
couchy README
|
||||
|
||||
Requirements :
|
||||
- werkzeug : http://werkzeug.pocoo.org
|
||||
- jinja : http://jinja.pocoo.org
|
||||
- couchdb 0.72 & above : https://couchdb.apache.org/
|
||||
- couchdb-python 0.3 & above : https://github.com/djc/couchdb-python
|
|
@ -0,0 +1,47 @@
|
|||
from couchdb.client import Server
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.wsgi import ClosingIterator
|
||||
|
||||
from . import views
|
||||
from .models import URL
|
||||
from .utils import local
|
||||
from .utils import local_manager
|
||||
from .utils import STATIC_PATH
|
||||
from .utils import url_map
|
||||
|
||||
|
||||
class Couchy:
|
||||
def __init__(self, db_uri):
|
||||
local.application = self
|
||||
|
||||
server = Server(db_uri)
|
||||
try:
|
||||
db = server.create("urls")
|
||||
except Exception:
|
||||
db = server["urls"]
|
||||
self.dispatch = SharedDataMiddleware(self.dispatch, {"/static": STATIC_PATH})
|
||||
|
||||
URL.db = db
|
||||
|
||||
def dispatch(self, environ, start_response):
|
||||
local.application = self
|
||||
request = Request(environ)
|
||||
local.url_adapter = adapter = url_map.bind_to_environ(environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
handler = getattr(views, endpoint)
|
||||
response = handler(request, **values)
|
||||
except NotFound:
|
||||
response = views.not_found(request)
|
||||
response.status_code = 404
|
||||
except HTTPException as e:
|
||||
response = e
|
||||
return ClosingIterator(
|
||||
response(environ, start_response), [local_manager.cleanup]
|
||||
)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.dispatch(environ, start_response)
|
|
@ -0,0 +1,50 @@
|
|||
from datetime import datetime
|
||||
|
||||
from couchdb.mapping import BooleanField
|
||||
from couchdb.mapping import DateTimeField
|
||||
from couchdb.mapping import Document
|
||||
from couchdb.mapping import TextField
|
||||
|
||||
from .utils import get_random_uid
|
||||
from .utils import url_for
|
||||
|
||||
|
||||
class URL(Document):
|
||||
target = TextField()
|
||||
public = BooleanField()
|
||||
added = DateTimeField(default=datetime.utcnow())
|
||||
shorty_id = TextField(default=None)
|
||||
db = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, id):
|
||||
return super().load(URL.db, id)
|
||||
|
||||
@classmethod
|
||||
def query(cls, code):
|
||||
return URL.db.query(code)
|
||||
|
||||
def store(self):
|
||||
if getattr(self._data, "id", None) is None:
|
||||
new_id = self.shorty_id if self.shorty_id else None
|
||||
while 1:
|
||||
id = new_id if new_id else get_random_uid()
|
||||
try:
|
||||
docid = URL.db.resource.put(content=self._data, path=f"/{id}/")[
|
||||
"id"
|
||||
]
|
||||
except Exception:
|
||||
continue
|
||||
if docid:
|
||||
break
|
||||
self._data = URL.db.get(docid)
|
||||
else:
|
||||
super().store(URL.db)
|
||||
return self
|
||||
|
||||
@property
|
||||
def short_url(self):
|
||||
return url_for("link", uid=self.id, _external=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<URL {self.id!r}>"
|
|
@ -0,0 +1,108 @@
|
|||
body {
|
||||
background-color: #333;
|
||||
font-family: 'Lucida Sans', 'Verdana', sans-serif;
|
||||
font-size: 16px;
|
||||
margin: 3em 0 3em 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0C4850;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #1C818F;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 500px;
|
||||
background-color: #24C0CE;
|
||||
text-align: center;
|
||||
font-size: 3em;
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
display: block;
|
||||
padding: 0.3em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
color: #ADEEF7;
|
||||
background-color: #0E8A96;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
margin: 0 auto 0 auto;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
padding: 10px;
|
||||
width: 480px;
|
||||
background-color: #004C63;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #A0E9FF;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 20px;
|
||||
width: 460px;
|
||||
background-color: #98CE24;
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.body h2 {
|
||||
margin: 0 0 0.5em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.body input {
|
||||
margin: 0.2em 0 0.2em 0;
|
||||
font-family: 'Lucida Sans', 'Verdana', sans-serif;
|
||||
font-size: 20px;
|
||||
background-color: #CCEB98;
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.body #url {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
div.body #alias {
|
||||
width: 300px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
div.body #submit {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
div.body p {
|
||||
margin: 0;
|
||||
padding: 0.2em 0 0.2em 0;
|
||||
}
|
||||
|
||||
div.body ul {
|
||||
margin: 1em 0 1em 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.error {
|
||||
margin: 1em 0 1em 0;
|
||||
border: 2px solid #AC0202;
|
||||
background-color: #9E0303;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.pagination {
|
||||
font-size: 13px;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
<h2>Shortened URL</h2>
|
||||
<p>
|
||||
The URL {{ url.target|urlize(40, true) }}
|
||||
was shortened to {{ url.short_url|urlize }}.
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Shorty</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', file='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="{{ url_for('new') }}">Shorty</a></h1>
|
||||
<div class="body">{% block body %}{% endblock %}</div>
|
||||
<div class="footer">
|
||||
<a href="{{ url_for('new') }}">new</a> |
|
||||
<a href="{{ url_for('list') }}">list</a> |
|
||||
use shorty for good, not for evil
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
<h2>List of URLs</h2>
|
||||
<ul>
|
||||
{%- for url in pagination.entries %}
|
||||
<li><a href="{{ url.short_url|e }}">{{ url.id|e }}</a> »
|
||||
<small>{{ url.target|urlize(38, true) }}</small></li>
|
||||
{%- else %}
|
||||
<li><em>no URLs shortened yet</em></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
<div class="pagination">
|
||||
{%- if pagination.has_previous %}<a href="{{ pagination.previous }}">« Previous</a>
|
||||
{%- else %}<span class="inactive">« Previous</span>{% endif %}
|
||||
| {{ pagination.page }} |
|
||||
{% if pagination.has_next %}<a href="{{ pagination.next }}">Next »</a>
|
||||
{%- else %}<span class="inactive">Next »</span>{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
<h2>Create a Shorty-URL!</h2>
|
||||
{% if error %}<div class="error">{{ error }}</div>{% endif -%}
|
||||
<form action="" method="post">
|
||||
<p>Enter the URL you want to shorten</p>
|
||||
<p><input type="text" name="url" id="url" value="{{ url|e(true) }}"></p>
|
||||
<p>Optionally you can give the URL a memorable name</p>
|
||||
<p><input type="text" id="alias" name="alias">{#
|
||||
#}<input type="submit" id="submit" value="Do!"></p>
|
||||
<p><input type="checkbox" name="private" id="private">
|
||||
<label for="private">make this URL private, so don't list it</label></p>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
<h2>Page Not Found</h2>
|
||||
<p>
|
||||
The page you have requested does not exist on this server. What about
|
||||
<a href="{{ url_for('new') }}">adding a new URL</a>?
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,101 @@
|
|||
from os import path
|
||||
from random import randrange
|
||||
from random import sample
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from werkzeug.local import Local
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.routing import Map
|
||||
from werkzeug.routing import Rule
|
||||
from werkzeug.urls import url_parse
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
TEMPLATE_PATH = path.join(path.dirname(__file__), "templates")
|
||||
STATIC_PATH = path.join(path.dirname(__file__), "static")
|
||||
ALLOWED_SCHEMES = frozenset(["http", "https", "ftp", "ftps"])
|
||||
URL_CHARS = "abcdefghijkmpqrstuvwxyzABCDEFGHIJKLMNPQRST23456789"
|
||||
|
||||
local = Local()
|
||||
local_manager = LocalManager([local])
|
||||
application = local("application")
|
||||
|
||||
url_map = Map([Rule("/static/<file>", endpoint="static", build_only=True)])
|
||||
|
||||
jinja_env = Environment(loader=FileSystemLoader(TEMPLATE_PATH))
|
||||
|
||||
|
||||
def expose(rule, **kw):
|
||||
def decorate(f):
|
||||
kw["endpoint"] = f.__name__
|
||||
url_map.add(Rule(rule, **kw))
|
||||
return f
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def url_for(endpoint, _external=False, **values):
|
||||
return local.url_adapter.build(endpoint, values, force_external=_external)
|
||||
|
||||
|
||||
jinja_env.globals["url_for"] = url_for
|
||||
|
||||
|
||||
def render_template(template, **context):
|
||||
return Response(
|
||||
jinja_env.get_template(template).render(**context), mimetype="text/html"
|
||||
)
|
||||
|
||||
|
||||
def validate_url(url):
|
||||
return url_parse(url)[0] in ALLOWED_SCHEMES
|
||||
|
||||
|
||||
def get_random_uid():
|
||||
return "".join(sample(URL_CHARS, randrange(3, 9)))
|
||||
|
||||
|
||||
class Pagination:
|
||||
def __init__(self, results, per_page, page, endpoint):
|
||||
self.results = results
|
||||
self.per_page = per_page
|
||||
self.page = page
|
||||
self.endpoint = endpoint
|
||||
|
||||
@cached_property
|
||||
def count(self):
|
||||
return len(self.results)
|
||||
|
||||
@cached_property
|
||||
def entries(self):
|
||||
return self.results[
|
||||
((self.page - 1) * self.per_page) : (
|
||||
((self.page - 1) * self.per_page) + self.per_page
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def has_previous(self):
|
||||
"""Return True if there are pages before the current one."""
|
||||
return self.page > 1
|
||||
|
||||
@property
|
||||
def has_next(self):
|
||||
"""Return True if there are pages after the current one."""
|
||||
return self.page < self.pages
|
||||
|
||||
@property
|
||||
def previous(self):
|
||||
"""Return the URL for the previous page."""
|
||||
return url_for(self.endpoint, page=self.page - 1)
|
||||
|
||||
@property
|
||||
def next(self):
|
||||
"""Return the URL for the next page."""
|
||||
return url_for(self.endpoint, page=self.page + 1)
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
"""Return the number of pages."""
|
||||
return max(0, self.count - 1) // self.per_page + 1
|
|
@ -0,0 +1,73 @@
|
|||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from .models import URL
|
||||
from .utils import expose
|
||||
from .utils import Pagination
|
||||
from .utils import render_template
|
||||
from .utils import url_for
|
||||
from .utils import validate_url
|
||||
|
||||
|
||||
@expose("/")
|
||||
def new(request):
|
||||
error = url = ""
|
||||
if request.method == "POST":
|
||||
url = request.form.get("url")
|
||||
alias = request.form.get("alias")
|
||||
if not validate_url(url):
|
||||
error = "I'm sorry but you cannot shorten this URL."
|
||||
elif alias:
|
||||
if len(alias) > 140:
|
||||
error = "Your alias is too long"
|
||||
elif "/" in alias:
|
||||
error = "Your alias might not include a slash"
|
||||
elif URL.load(alias):
|
||||
error = "The alias you have requested exists already"
|
||||
if not error:
|
||||
url = URL(
|
||||
target=url,
|
||||
public="private" not in request.form,
|
||||
shorty_id=alias if alias else None,
|
||||
)
|
||||
url.store()
|
||||
uid = url.id
|
||||
return redirect(url_for("display", uid=uid))
|
||||
return render_template("new.html", error=error, url=url)
|
||||
|
||||
|
||||
@expose("/display/<uid>")
|
||||
def display(request, uid):
|
||||
url = URL.load(uid)
|
||||
if not url:
|
||||
raise NotFound()
|
||||
return render_template("display.html", url=url)
|
||||
|
||||
|
||||
@expose("/u/<uid>")
|
||||
def link(request, uid):
|
||||
url = URL.load(uid)
|
||||
if not url:
|
||||
raise NotFound()
|
||||
return redirect(url.target, 301)
|
||||
|
||||
|
||||
@expose("/list/", defaults={"page": 1})
|
||||
@expose("/list/<int:page>")
|
||||
def list(request, page):
|
||||
def wrap(doc):
|
||||
data = doc.value
|
||||
data["_id"] = doc.id
|
||||
return URL.wrap(data)
|
||||
|
||||
code = """function(doc) { if (doc.public){ map([doc._id], doc); }}"""
|
||||
docResults = URL.query(code)
|
||||
results = [wrap(doc) for doc in docResults]
|
||||
pagination = Pagination(results, 1, page, "list")
|
||||
if pagination.page > 1 and not pagination.entries:
|
||||
raise NotFound()
|
||||
return render_template("list.html", pagination=pagination)
|
||||
|
||||
|
||||
def not_found(request):
|
||||
return render_template("not_found.html")
|
|
@ -0,0 +1,2 @@
|
|||
"""Werkzeug powered Teeworlds Server Browser."""
|
||||
from .application import make_app
|
|
@ -0,0 +1,120 @@
|
|||
import time
|
||||
from os import path
|
||||
from threading import Thread
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import PackageLoader
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
from werkzeug.routing import Map
|
||||
from werkzeug.routing import Rule
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from .db import Database
|
||||
from .network import ServerBrowser
|
||||
|
||||
|
||||
templates = path.join(path.dirname(__file__), "templates")
|
||||
pages = {}
|
||||
url_map = Map([Rule("/shared/<file>", endpoint="shared")])
|
||||
|
||||
|
||||
def make_app(database, interval=120):
|
||||
return SharedDataMiddleware(
|
||||
Cup(database, interval),
|
||||
{"/shared": path.join(path.dirname(__file__), "shared")},
|
||||
)
|
||||
|
||||
|
||||
class PageMeta(type):
|
||||
def __init__(cls, name, bases, d):
|
||||
type.__init__(cls, name, bases, d)
|
||||
if d.get("url_rule") is not None:
|
||||
pages[cls.identifier] = cls
|
||||
url_map.add(
|
||||
Rule(cls.url_rule, endpoint=cls.identifier, **cls.url_arguments)
|
||||
)
|
||||
|
||||
@property
|
||||
def identifier(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
|
||||
def _with_metaclass(meta, *bases):
|
||||
"""Create a base class with a metaclass."""
|
||||
|
||||
class metaclass(type):
|
||||
def __new__(metacls, name, this_bases, d):
|
||||
return meta(name, bases, d)
|
||||
|
||||
return type.__new__(metaclass, "temporary_class", (), {})
|
||||
|
||||
|
||||
class Page(_with_metaclass(PageMeta, object)):
|
||||
url_arguments = {}
|
||||
|
||||
def __init__(self, cup, request, url_adapter):
|
||||
self.cup = cup
|
||||
self.request = request
|
||||
self.url_adapter = url_adapter
|
||||
|
||||
def url_for(self, endpoint, **values):
|
||||
return self.url_adapter.build(endpoint, values)
|
||||
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
def render_template(self, template=None):
|
||||
if template is None:
|
||||
template = f"{type(self).identifier}.html"
|
||||
context = dict(self.__dict__)
|
||||
context.update(url_for=self.url_for, self=self)
|
||||
return self.cup.render_template(template, context)
|
||||
|
||||
def get_response(self):
|
||||
return Response(self.render_template(), mimetype="text/html")
|
||||
|
||||
|
||||
class Cup:
|
||||
def __init__(self, database, interval=120):
|
||||
self.jinja_env = Environment(loader=PackageLoader("cupoftee"), autoescape=True)
|
||||
self.interval = interval
|
||||
self.db = Database(database)
|
||||
self.server_browser = ServerBrowser(self)
|
||||
self.updater = Thread(None, self.update_server_browser)
|
||||
self.updater.daemon = True
|
||||
self.updater.start()
|
||||
|
||||
def update_server_browser(self):
|
||||
while 1:
|
||||
if self.server_browser.sync():
|
||||
wait = self.interval
|
||||
else:
|
||||
wait = self.interval // 2
|
||||
time.sleep(wait)
|
||||
|
||||
def dispatch_request(self, request):
|
||||
url_adapter = url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = url_adapter.match()
|
||||
page = pages[endpoint](self, request, url_adapter)
|
||||
response = page.process(**values)
|
||||
except NotFound:
|
||||
page = MissingPage(self, request, url_adapter)
|
||||
response = page.process()
|
||||
except HTTPException as e:
|
||||
return e
|
||||
return response or page.get_response()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
return self.dispatch_request(request)(environ, start_response)
|
||||
|
||||
def render_template(self, name, **context):
|
||||
template = self.jinja_env.get_template(name)
|
||||
return template.render(context)
|
||||
|
||||
|
||||
from cupoftee.pages import MissingPage
|
|
@ -0,0 +1,67 @@
|
|||
"""A simple object database. As long as the server is not running in
|
||||
multiprocess mode that's good enough.
|
||||
"""
|
||||
import dbm
|
||||
from pickle import dumps
|
||||
from pickle import loads
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self._fs = dbm.open(filename, "cf")
|
||||
self._local = {}
|
||||
self._lock = Lock()
|
||||
|
||||
def __getitem__(self, key):
|
||||
with self._lock:
|
||||
return self._load_key(key)
|
||||
|
||||
def _load_key(self, key):
|
||||
if key in self._local:
|
||||
return self._local[key]
|
||||
rv = loads(self._fs[key])
|
||||
self._local[key] = rv
|
||||
return rv
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._local[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
with self._lock:
|
||||
self._local.pop(key, None)
|
||||
if key in self._fs:
|
||||
del self._fs[key]
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __contains__(self, key):
|
||||
with self._lock:
|
||||
try:
|
||||
self._load_key(key)
|
||||
except KeyError:
|
||||
pass
|
||||
return key in self._local
|
||||
|
||||
def setdefault(self, key, factory):
|
||||
with self._lock:
|
||||
try:
|
||||
rv = self._load_key(key)
|
||||
except KeyError:
|
||||
self._local[key] = rv = factory()
|
||||
return rv
|
||||
|
||||
def sync(self):
|
||||
with self._lock:
|
||||
for key, value in self._local.items():
|
||||
self._fs[key] = dumps(value, 2)
|
||||
self._fs.sync()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.sync()
|
||||
self._fs.close()
|
||||
except Exception:
|
||||
pass
|
|
@ -0,0 +1,124 @@
|
|||
"""Query the servers for information."""
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from math import log
|
||||
|
||||
from .utils import unicodecmp
|
||||
|
||||
|
||||
class ServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Syncable:
|
||||
last_sync = None
|
||||
|
||||
def sync(self):
|
||||
try:
|
||||
self._sync()
|
||||
except (OSError, socket.timeout):
|
||||
return False
|
||||
self.last_sync = datetime.utcnow()
|
||||
return True
|
||||
|
||||
|
||||
class ServerBrowser(Syncable):
|
||||
def __init__(self, cup):
|
||||
self.cup = cup
|
||||
self.servers = cup.db.setdefault("servers", dict)
|
||||
|
||||
def _sync(self):
|
||||
to_delete = set(self.servers)
|
||||
for x in range(1, 17):
|
||||
addr = (f"master{x}.teeworlds.com", 8300)
|
||||
print(addr)
|
||||
try:
|
||||
self._sync_server_browser(addr, to_delete)
|
||||
except (OSError, socket.timeout):
|
||||
continue
|
||||
for server_id in to_delete:
|
||||
self.servers.pop(server_id, None)
|
||||
if not self.servers:
|
||||
raise OSError("no servers found")
|
||||
self.cup.db.sync()
|
||||
|
||||
def _sync_server_browser(self, addr, to_delete):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(5)
|
||||
s.sendto(b"\x20\x00\x00\x00\x00\x48\xff\xff\xff\xffreqt", addr)
|
||||
data = s.recvfrom(1024)[0][14:]
|
||||
s.close()
|
||||
|
||||
for n in range(0, len(data) // 6):
|
||||
addr = (
|
||||
".".join(map(str, map(ord, data[n * 6 : n * 6 + 4]))),
|
||||
ord(data[n * 6 + 5]) * 256 + ord(data[n * 6 + 4]),
|
||||
)
|
||||
server_id = f"{addr[0]}:{addr[1]}"
|
||||
if server_id in self.servers:
|
||||
if not self.servers[server_id].sync():
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
self.servers[server_id] = Server(addr, server_id)
|
||||
except ServerError:
|
||||
pass
|
||||
to_delete.discard(server_id)
|
||||
|
||||
|
||||
class Server(Syncable):
|
||||
def __init__(self, addr, server_id):
|
||||
self.addr = addr
|
||||
self.id = server_id
|
||||
self.players = []
|
||||
if not self.sync():
|
||||
raise ServerError("server not responding in time")
|
||||
|
||||
def _sync(self):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(1)
|
||||
s.sendto(b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffgief", self.addr)
|
||||
bits = s.recvfrom(1024)[0][14:].split(b"\x00")
|
||||
s.close()
|
||||
self.version, server_name, map_name = bits[:3]
|
||||
self.name = server_name.decode("latin1")
|
||||
self.map = map_name.decode("latin1")
|
||||
self.gametype = bits[3]
|
||||
self.flags, self.progression, player_count, self.max_players = map(
|
||||
int, bits[4:8]
|
||||
)
|
||||
|
||||
# sync the player stats
|
||||
players = {p.name: p for p in self.players}
|
||||
for i in range(player_count):
|
||||
name = bits[8 + i * 2].decode("latin1")
|
||||
score = int(bits[9 + i * 2])
|
||||
|
||||
# update existing player
|
||||
if name in players:
|
||||
player = players.pop(name)
|
||||
player.score = score
|
||||
# add new player
|
||||
else:
|
||||
self.players.append(Player(self, name, score))
|
||||
# delete players that left
|
||||
for player in players.values():
|
||||
try:
|
||||
self.players.remove(player)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# sort the player list and count them
|
||||
self.players.sort(key=lambda x: -x.score)
|
||||
self.player_count = len(self.players)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return unicodecmp(self.name, other.name)
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, server, name, score):
|
||||
self.server = server
|
||||
self.name = name
|
||||
self.score = score
|
||||
self.size = round(100 + log(max(score, 1)) * 25, 2)
|
|
@ -0,0 +1,75 @@
|
|||
from functools import reduce
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from .application import Page
|
||||
from .utils import unicodecmp
|
||||
|
||||
|
||||
class ServerList(Page):
|
||||
url_rule = "/"
|
||||
|
||||
def order_link(self, name, title):
|
||||
cls = ""
|
||||
link = f"?order_by={name}"
|
||||
desc = False
|
||||
if name == self.order_by:
|
||||
desc = not self.order_desc
|
||||
cls = f' class="{"down" if desc else "up"}"'
|
||||
if desc:
|
||||
link += "&dir=desc"
|
||||
return f'<a href="{link}"{cls}>{title}</a>'
|
||||
|
||||
def process(self):
|
||||
self.order_by = self.request.args.get("order_by") or "name"
|
||||
sort_func = {
|
||||
"name": lambda x: x,
|
||||
"map": lambda x: x.map,
|
||||
"gametype": lambda x: x.gametype,
|
||||
"players": lambda x: x.player_count,
|
||||
"progression": lambda x: x.progression,
|
||||
}.get(self.order_by)
|
||||
if sort_func is None:
|
||||
return redirect(self.url_for("serverlist"))
|
||||
|
||||
self.servers = self.cup.server_browser.servers.values()
|
||||
self.servers.sort(key=sort_func)
|
||||
if self.request.args.get("dir") == "desc":
|
||||
self.servers.reverse()
|
||||
self.order_desc = True
|
||||
else:
|
||||
self.order_desc = False
|
||||
|
||||
self.players = reduce(lambda a, b: a + b.players, self.servers, [])
|
||||
self.players = sorted(self.players, key=lambda a, b: unicodecmp(a.name, b.name))
|
||||
|
||||
|
||||
class Server(Page):
|
||||
url_rule = "/server/<id>"
|
||||
|
||||
def process(self, id):
|
||||
try:
|
||||
self.server = self.cup.server_browser.servers[id]
|
||||
except KeyError:
|
||||
raise NotFound() from None
|
||||
|
||||
|
||||
class Search(Page):
|
||||
url_rule = "/search"
|
||||
|
||||
def process(self):
|
||||
self.user = self.request.args.get("user")
|
||||
if self.user:
|
||||
self.results = []
|
||||
for server in self.cup.server_browser.servers.values():
|
||||
for player in server.players:
|
||||
if player.name == self.user:
|
||||
self.results.append(server)
|
||||
|
||||
|
||||
class MissingPage(Page):
|
||||
def get_response(self):
|
||||
response = super().get_response()
|
||||
response.status_code = 404
|
||||
return response
|
After Width: | Height: | Size: 230 B |
After Width: | Height: | Size: 376 B |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 49 KiB |