conflict rez + pip3 install cherrypy
This commit is contained in:
commit
cbcdd3e14c
8
awesome_venv/bin/calc-prorate
Executable file
8
awesome_venv/bin/calc-prorate
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from tempora import calculate_prorated_values
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(calculate_prorated_values())
|
||||
8
awesome_venv/bin/cheroot
Executable file
8
awesome_venv/bin/cheroot
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from cheroot.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
awesome_venv/bin/cherryd
Executable file
8
awesome_venv/bin/cherryd
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from cherrypy.__main__ import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,30 @@
|
||||
Copyright © 2004-2019, CherryPy Team (team@cherrypy.dev)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
* * *
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* 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.
|
||||
|
||||
* Neither the name of CherryPy 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,201 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: CherryPy
|
||||
Version: 18.9.0
|
||||
Summary: Object-Oriented HTTP framework
|
||||
Home-page: https://www.cherrypy.dev
|
||||
Author: CherryPy Team
|
||||
Author-email: team@cherrypy.dev
|
||||
Project-URL: CI: AppVeyor, https://ci.appveyor.com/project/cherrypy/cherrypy
|
||||
Project-URL: CI: Travis, https://travis-ci.org/cherrypy/cherrypy
|
||||
Project-URL: CI: Circle, https://circleci.com/gh/cherrypy/cherrypy
|
||||
Project-URL: CI: GitHub, https://github.com/cherrypy/cherrypy/actions
|
||||
Project-URL: Docs: RTD, https://docs.cherrypy.dev
|
||||
Project-URL: GitHub: issues, https://github.com/cherrypy/cherrypy/issues
|
||||
Project-URL: GitHub: repo, https://github.com/cherrypy/cherrypy
|
||||
Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: Freely Distributable
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Framework :: CherryPy
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: Implementation
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: Jython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Internet :: WWW/HTTP
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Requires-Python: >=3.6
|
||||
License-File: LICENSE.md
|
||||
Requires-Dist: cheroot >=8.2.1
|
||||
Requires-Dist: portend >=2.1.1
|
||||
Requires-Dist: more-itertools
|
||||
Requires-Dist: zc.lockfile
|
||||
Requires-Dist: jaraco.collections
|
||||
Requires-Dist: pywin32 >=227 ; sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10"
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx ; extra == 'docs'
|
||||
Requires-Dist: docutils ; extra == 'docs'
|
||||
Requires-Dist: alabaster ; extra == 'docs'
|
||||
Requires-Dist: sphinxcontrib-apidoc >=0.3.0 ; extra == 'docs'
|
||||
Requires-Dist: rst.linker >=1.11 ; extra == 'docs'
|
||||
Requires-Dist: jaraco.packaging >=3.2 ; extra == 'docs'
|
||||
Requires-Dist: setuptools ; extra == 'docs'
|
||||
Provides-Extra: json
|
||||
Requires-Dist: simplejson ; extra == 'json'
|
||||
Provides-Extra: memcached_session
|
||||
Requires-Dist: python-memcached >=1.58 ; extra == 'memcached_session'
|
||||
Provides-Extra: routes_dispatcher
|
||||
Requires-Dist: routes >=2.3.1 ; extra == 'routes_dispatcher'
|
||||
Provides-Extra: ssl
|
||||
Requires-Dist: pyOpenSSL ; extra == 'ssl'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: coverage ; extra == 'testing'
|
||||
Requires-Dist: codecov ; extra == 'testing'
|
||||
Requires-Dist: objgraph ; extra == 'testing'
|
||||
Requires-Dist: pytest >=5.3.5 ; extra == 'testing'
|
||||
Requires-Dist: pytest-cov ; extra == 'testing'
|
||||
Requires-Dist: pytest-forked ; extra == 'testing'
|
||||
Requires-Dist: pytest-sugar ; extra == 'testing'
|
||||
Requires-Dist: path.py ; extra == 'testing'
|
||||
Requires-Dist: requests-toolbelt ; extra == 'testing'
|
||||
Requires-Dist: pytest-services >=2 ; extra == 'testing'
|
||||
Requires-Dist: setuptools ; extra == 'testing'
|
||||
Provides-Extra: xcgi
|
||||
Requires-Dist: flup ; extra == 'xcgi'
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg
|
||||
:target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md
|
||||
:alt: SWUbanner
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/cherrypy.svg
|
||||
:target: https://pypi.org/project/cherrypy
|
||||
|
||||
.. image:: https://tidelift.com/badges/package/pypi/CherryPy
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=readme
|
||||
:alt: CherryPy is available as part of the Tidelift Subscription
|
||||
|
||||
.. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg
|
||||
:target: https://python3statement.org/
|
||||
|
||||
.. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg
|
||||
:target: https://python3statement.org/#sections40-timeline
|
||||
|
||||
|
||||
|
||||
.. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest
|
||||
:target: https://docs.cherrypy.dev/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg
|
||||
:target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy
|
||||
|
||||
.. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg
|
||||
:target: https://groups.google.com/group/cherrypy-users
|
||||
|
||||
.. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg
|
||||
:target: https://gitter.im/cherrypy/cherrypy
|
||||
|
||||
.. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI
|
||||
:target: https://travis-ci.org/cherrypy/cherrypy
|
||||
|
||||
.. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg
|
||||
:target: https://circleci.com/gh/cherrypy/cherrypy/tree/master
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor
|
||||
:target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600
|
||||
:target: https://pypi.org/project/cheroot
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/cherrypy.svg
|
||||
:target: https://pypi.org/project/cherrypy
|
||||
|
||||
.. image:: https://badges.github.io/stability-badges/dist/stable.svg
|
||||
:target: https://github.com/badges/stability-badges
|
||||
:alt: stable
|
||||
|
||||
.. image:: https://api.codacy.com/project/badge/Grade/48b11060b5d249dc86e52dac2be2c715
|
||||
:target: https://www.codacy.com/app/webknjaz/cherrypy-upstream?utm_source=github.com&utm_medium=referral&utm_content=cherrypy/cherrypy&utm_campaign=Badge_Grade
|
||||
|
||||
.. image:: https://codecov.io/gh/cherrypy/cherrypy/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/cherrypy/cherrypy
|
||||
:alt: codecov
|
||||
|
||||
Welcome to the GitHub repository of `CherryPy <https://cherrypy.dev>`_!
|
||||
|
||||
CherryPy is a pythonic, object-oriented HTTP framework.
|
||||
|
||||
1. It allows building web applications in much the same way one would
|
||||
build any other object-oriented program.
|
||||
2. This design results in more concise and readable code developed faster.
|
||||
It's all just properties and methods.
|
||||
3. It is now more than ten years old and has proven fast and very
|
||||
stable.
|
||||
4. It is being used in production by many sites, from the simplest to
|
||||
the most demanding.
|
||||
5. And perhaps most importantly, it is fun to work with :-)
|
||||
|
||||
Here's how easy it is to write "Hello World" in CherryPy:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import cherrypy
|
||||
|
||||
class HelloWorld(object):
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello World!"
|
||||
|
||||
cherrypy.quickstart(HelloWorld())
|
||||
|
||||
And it continues to work that intuitively when systems grow, allowing
|
||||
for the Python object model to be dynamically presented as a website
|
||||
and/or API.
|
||||
|
||||
While CherryPy is one of the easiest and most intuitive frameworks out
|
||||
there, the prerequisite for understanding the `CherryPy
|
||||
documentation <https://docs.cherrypy.dev>`_ is that you have
|
||||
a general understanding of Python and web development.
|
||||
Additionally:
|
||||
|
||||
- Tutorials are included in the repository:
|
||||
https://github.com/cherrypy/cherrypy/tree/master/cherrypy/tutorial
|
||||
- A general wiki at:
|
||||
https://github.com/cherrypy/cherrypy/wiki
|
||||
|
||||
If the docs are insufficient to address your needs, the CherryPy
|
||||
community has several `avenues for support
|
||||
<https://docs.cherrypy.dev/en/latest/support.html>`_.
|
||||
|
||||
For Enterprise
|
||||
--------------
|
||||
|
||||
CherryPy is available as part of the Tidelift Subscription.
|
||||
|
||||
The CherryPy maintainers and the maintainers of thousands of other packages
|
||||
are working with Tidelift to deliver one enterprise subscription that covers
|
||||
all of the open source you use.
|
||||
|
||||
`Learn more <https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=github>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Please follow the `contribution guidelines
|
||||
<https://docs.cherrypy.dev/en/latest/contribute.html>`_.
|
||||
And by all means, absorb the `Zen of
|
||||
CherryPy <https://github.com/cherrypy/cherrypy/wiki/The-Zen-of-CherryPy>`_.
|
||||
@ -0,0 +1,235 @@
|
||||
../../../bin/cherryd,sha256=L7hGGldd0BmJ_lR4GGKlOimkNa-HMk7b9cUOmX9jk1Q,247
|
||||
CherryPy-18.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
CherryPy-18.9.0.dist-info/LICENSE.md,sha256=Ra3pM8KA7ON-PgHTqRr-7ZUFdGpFb3KtELCJfEnmUQQ,1511
|
||||
CherryPy-18.9.0.dist-info/METADATA,sha256=n2c4v1oJ39Pk7RfnGD3efAoXB3QDX-f7z3kWYR1VtAQ,8750
|
||||
CherryPy-18.9.0.dist-info/RECORD,,
|
||||
CherryPy-18.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
CherryPy-18.9.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
||||
CherryPy-18.9.0.dist-info/entry_points.txt,sha256=2iMzTbV4_iDQUVEBHZGAuXaFAbW9naJoL4O9Gn7eDks,50
|
||||
CherryPy-18.9.0.dist-info/top_level.txt,sha256=mOBE-r7Ej1kFrXNKlOj3yY9QfGA6Xkz6vZK7VNJF3YE,9
|
||||
cherrypy/__init__.py,sha256=BHFq6fz2ILLDGq7OCALzmX8MaxPGI6AiR-fYLJo1kIw,11285
|
||||
cherrypy/__main__.py,sha256=gb89ZhhQ1kjYvU61axGuZt5twA4q3kfBa3WKrPT6D2k,107
|
||||
cherrypy/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/__main__.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpchecker.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpcompat.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpconfig.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpdispatch.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cperror.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cplogging.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpmodpy.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpnative_server.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpreqbody.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cprequest.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpserver.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cptools.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cptree.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpwsgi.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_cpwsgi_server.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_helper.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/_json.cpython-310.pyc,,
|
||||
cherrypy/__pycache__/daemon.cpython-310.pyc,,
|
||||
cherrypy/_cpchecker.py,sha256=aAZfQTmW7_f0MfKZmmaHKs_7IjeOi8j-voeDmSbLEA0,14566
|
||||
cherrypy/_cpcompat.py,sha256=2gZtFj0h6RRGlkY-R6WoEl-S8BBSIwCeuj7F2Tf3mU0,1992
|
||||
cherrypy/_cpconfig.py,sha256=abftUSXmOWoqpak3VHOR50XBBo6wxy8qngz5VmsRXDQ,9647
|
||||
cherrypy/_cpdispatch.py,sha256=BRpBxNHxyPmfuRCdjwUNiwgkwyPnpQQ1p4_htSDicoU,25216
|
||||
cherrypy/_cperror.py,sha256=Maxb8U0CJfEHkwI678a-H8PXnZvWcObnr7_GW1nVvNc,23103
|
||||
cherrypy/_cplogging.py,sha256=Y5eBiAKW1jdL2ikzGfawWKYVkKHvfMU92I-vsnnbSZQ,16438
|
||||
cherrypy/_cpmodpy.py,sha256=lUqckVgqRr972jrFyA0y2TD79gA8NaRj5nboDuuARDY,11096
|
||||
cherrypy/_cpnative_server.py,sha256=BtmMuMtdBmjb_kavTt6NdoREmqkHRhi1bUAG9EdhQuU,6677
|
||||
cherrypy/_cpreqbody.py,sha256=85XtrtUq6wd1XeyRK2i2wiSY3QvJfaPnoLGMhhaks68,36382
|
||||
cherrypy/_cprequest.py,sha256=fIGuIbJ7Eb_-zriV4300VG7x7f0f7xXzTzxVez6fZVA,34408
|
||||
cherrypy/_cpserver.py,sha256=sai48_tDCkUrhqLKaSZSyk3aSIRxgUiMv6FNUe03PBY,8320
|
||||
cherrypy/_cptools.py,sha256=Y35JizxKfVy3JNCbVido0rSXlyIzv4MrYDxdRuYG0bQ,18163
|
||||
cherrypy/_cptree.py,sha256=_DLxtDfnPksYk5wlkLzsyCIk5TqrwE4wgkF1yFPdbko,10977
|
||||
cherrypy/_cpwsgi.py,sha256=W3u6BKh1P_TZw9d5DzuwZhINXeQcMGvUVshN0iaFEOk,16394
|
||||
cherrypy/_cpwsgi_server.py,sha256=ZILHc_ouGC_eARqrVTA1z5pdm9T5QLGURN4Idr0eiJQ,4187
|
||||
cherrypy/_helper.py,sha256=bTZvfBpsZ0FDAGqtuuSblaiDloogxCpOyBy-XJF39vE,11653
|
||||
cherrypy/_json.py,sha256=zxl6rLuW4xE5aiXJ0UPeI22NhdeLG-gDN_FFOMGikg0,440
|
||||
cherrypy/daemon.py,sha256=kgiqlnWFx-PkLEcPPRJmXmB6zbI7X0cN2cCRtosow4c,3950
|
||||
cherrypy/favicon.ico,sha256=jrNK5SnKfbnSFgX_xQyLX3khmeImw8IbbHgJVIsGci0,1406
|
||||
cherrypy/lib/__init__.py,sha256=5_heysJFsUQMDLVNEFuCUbH03wWbA3lqsErmXLvPfFU,2745
|
||||
cherrypy/lib/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/auth_basic.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/auth_digest.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/caching.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/covercp.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/cpstats.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/cptools.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/encoding.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/gctools.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/httputil.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/jsontools.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/locking.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/profiler.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/reprconf.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/sessions.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/static.cpython-310.pyc,,
|
||||
cherrypy/lib/__pycache__/xmlrpcutil.cpython-310.pyc,,
|
||||
cherrypy/lib/auth_basic.py,sha256=FkySKMk0WC7OeLIefylkNyGORLpr9Rf98jQuxdBGHsA,4421
|
||||
cherrypy/lib/auth_digest.py,sha256=WO9aXkVvqj0nAsD1jRYN4zy9OvhUo7FdiXkgvfx3iYE,15351
|
||||
cherrypy/lib/caching.py,sha256=rJ_fPGlZ3Dpbg-BEDZQHL-m4_LJHJg69tBrAqgfzOW8,17517
|
||||
cherrypy/lib/covercp.py,sha256=l3zwygAxDBZ8x617-HPk4lUXN20wRqXdblTm_v2dPOc,11599
|
||||
cherrypy/lib/cpstats.py,sha256=luAeI7Dttzb3HhWUNsZUrNCrFp9LDXiBIOeVEzVmxiU,22854
|
||||
cherrypy/lib/cptools.py,sha256=wSn80P3to8QhHzgL2Q2OC6rmd0qOAFLb4ZQn-wpkLKk,23530
|
||||
cherrypy/lib/encoding.py,sha256=pnUVBiRioH9LpLU-K81u47yCviPebPNyX3P_Pzvv6Lw,17047
|
||||
cherrypy/lib/gctools.py,sha256=5SI3w507J3JJConDjp8Jmgo6FI9uR59S_mm44cGB_Lw,7346
|
||||
cherrypy/lib/httputil.py,sha256=304ixadmh6Tm5CGRqk7JaDKruKzeDJ7vJCdwRXPRBzQ,18007
|
||||
cherrypy/lib/jsontools.py,sha256=YQQmGQN4XKnSgB4u0lRl2MB9YkVeNR3-6YWyU76jUFQ,3641
|
||||
cherrypy/lib/locking.py,sha256=RLUwVj07-xNnBavmcRkjs-fmg712S7wSk3Gnox9TyEg,1224
|
||||
cherrypy/lib/profiler.py,sha256=_IegAnVjU5H2CVx8AFQzGOlYwKWVg42VE2rsMj5-zQA,6556
|
||||
cherrypy/lib/reprconf.py,sha256=zcAtqk5LSSnKgQHlT_IW5krbshsCBKhcFFaqJfSld1Q,12329
|
||||
cherrypy/lib/sessions.py,sha256=CEruNzLRCPiDbHWVL_FUTJHr0xVWvWdy05-ATTJNk60,30896
|
||||
cherrypy/lib/static.py,sha256=mdBINEDzuNQUdG07Hd6wh8DckugKNAgDBoq88nFFMVM,16612
|
||||
cherrypy/lib/xmlrpcutil.py,sha256=UZqJsoBboSHjgsSiOfFcDwlubtSruhYyxpKdxutazQk,1684
|
||||
cherrypy/process/__init__.py,sha256=RjaRqUG5U-ZhxAs7GBWN9PFR5hIK-9a9x3ZFwFjyW4Y,547
|
||||
cherrypy/process/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/process/__pycache__/plugins.cpython-310.pyc,,
|
||||
cherrypy/process/__pycache__/servers.cpython-310.pyc,,
|
||||
cherrypy/process/__pycache__/win32.cpython-310.pyc,,
|
||||
cherrypy/process/__pycache__/wspbus.cpython-310.pyc,,
|
||||
cherrypy/process/plugins.py,sha256=hZ0r1cpK5qQZCFmTG28GC6GTS1Gcd01bU06DYQc2ucw,26846
|
||||
cherrypy/process/servers.py,sha256=KojCzP2AxBV6D3URQX_blU8dfpkIL78OpfdwcQwRtt8,13442
|
||||
cherrypy/process/win32.py,sha256=o_aT4XrUL6WcyVPti4O59apFd5ARoJz75VbiktdqqJk,5787
|
||||
cherrypy/process/wspbus.py,sha256=_AFV7s2hmZngJF79reVWMD4rKK5i5wouLETfWiezV7U,21459
|
||||
cherrypy/scaffold/__init__.py,sha256=sLK5vjy-_f6ikJ7-Lhj7HT8VGAIi6olqPuZMTD-RpAQ,1997
|
||||
cherrypy/scaffold/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/scaffold/apache-fcgi.conf,sha256=0M10HHX8i2Or3r-gHoDglSQK4dZHd4Jhx4WhamJtuwc,930
|
||||
cherrypy/scaffold/example.conf,sha256=EAqr2Sb1B1osc198dY1FV2A0wgnBmGsJf99_-GGexVU,62
|
||||
cherrypy/scaffold/site.conf,sha256=pjUhF-ir1xzSsV7LqXGfyR6Ns_r_n3ATWw8OlfbgT3w,426
|
||||
cherrypy/scaffold/static/made_with_cherrypy_small.png,sha256=VlSRvYj-pZzls-peicQhWpbqkdsZHtNhPtSfZV12BFQ,6347
|
||||
cherrypy/test/__init__.py,sha256=jWQJbuVAcOuCITN7V8yrRz7U4rcK_ACx35Ephl4xwqY,396
|
||||
cherrypy/test/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/_test_decorators.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/_test_states_demo.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/benchmark.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/checkerdemo.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/helper.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/logtest.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/modfastcgi.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/modfcgid.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/modpy.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/modwsgi.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/sessiondemo.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_auth_basic.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_auth_digest.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_bus.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_caching.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_config.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_config_server.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_conn.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_core.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_dynamicobjectmapping.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_encoding.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_etags.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_http.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_httputil.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_iterator.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_json.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_logging.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_mime.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_misc_tools.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_native.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_objectmapping.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_params.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_plugins.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_proxy.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_refleaks.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_request_obj.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_routes.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_session.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_sessionauthenticate.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_states.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_static.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_tools.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_tutorials.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_virtualhost.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_wsgi_ns.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_wsgi_unix_socket.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_wsgi_vhost.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_wsgiapps.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/test_xmlrpc.cpython-310.pyc,,
|
||||
cherrypy/test/__pycache__/webtest.cpython-310.pyc,,
|
||||
cherrypy/test/_test_decorators.py,sha256=U5y0mhWwTcCrYLjY3EWSkkz_Vw-z0bPSP5ujwb6b79Y,951
|
||||
cherrypy/test/_test_states_demo.py,sha256=lVpbqHgHcIfdz1-i6xDbmzmD87V3JbwD_e4lK0Bagnw,1876
|
||||
cherrypy/test/benchmark.py,sha256=GoTeD98I5tFaDDSQTxmy1OrmTM5XS8bybg41i2j3rB4,12563
|
||||
cherrypy/test/checkerdemo.py,sha256=H8muNg9uHSAlDVYc-rICkcUKtJqzlTQfjYmMzC1Fuv4,1861
|
||||
cherrypy/test/fastcgi.conf,sha256=0YsIPLmOg-NdGGqCCPpBERKGYy_zBU6LkRDyR41nvBE,686
|
||||
cherrypy/test/fcgi.conf,sha256=neiD1sjiFblAJLUdlOSKiZ1uVl1eK3zM2_7LZQigkTs,486
|
||||
cherrypy/test/helper.py,sha256=y7PwhV4kZoJsXTUMXDtqGmZxk-7RfqWec7SvXWCOYto,16398
|
||||
cherrypy/test/logtest.py,sha256=zAMSL0AGGYwXUFLm56ztev6yb9GR94hL_wArtXC0Sic,8325
|
||||
cherrypy/test/modfastcgi.py,sha256=SblRPAhbo9uJLHISamWE7AyW2slrlWms76VvpnCqzLo,4607
|
||||
cherrypy/test/modfcgid.py,sha256=7a5iRZ9VQyr5O2Pajgo_73j6rbMr_i02FG6sO1-Q-lM,4192
|
||||
cherrypy/test/modpy.py,sha256=F6Ar3uxr1ckp5V5l6FNJEZnNHSB0SDtR5hGmYU010oY,4933
|
||||
cherrypy/test/modwsgi.py,sha256=DnKeIzaYSY2_fBgGb2ojjWS_A9A2UcuZUgneSalPO08,4789
|
||||
cherrypy/test/sessiondemo.py,sha256=_vVruGha_8f6Fim_n9LgxwNxebbM6JgiqBvyX0WaEQc,5425
|
||||
cherrypy/test/static/404.html,sha256=9jnU0KKbdzHVq9EvhkGOEly92qNJbtCSXOsH0KFTL4U,92
|
||||
cherrypy/test/static/dirback.jpg,sha256=eS_X3BSeu8OSu-GTYndM1tJkWoW_oVJp1O_mmUUGeo8,16585
|
||||
cherrypy/test/static/index.html,sha256=cB6ALrLhcxEGyMNgOHzmnBvmRnPCW_u3ebZUqdCiHkQ,14
|
||||
cherrypy/test/style.css,sha256=2Ypw_ziOWlY4dTZJlwsrorDLqLA1z485lgagaGemtKQ,17
|
||||
cherrypy/test/test.pem,sha256=x6LrLPw2dBRyZwHXk6FhdSDNM3-Cv7DBXc8o4A19RhI,2254
|
||||
cherrypy/test/test_auth_basic.py,sha256=JlLdHeyaEutMzWK7LX3HfSOVQWAKZXoUNL67sSgUMZQ,4499
|
||||
cherrypy/test/test_auth_digest.py,sha256=pp49xOWFS_i8N_s8MQ6h_exbOxlQZjfjT-cxDvi701E,4454
|
||||
cherrypy/test/test_bus.py,sha256=K_6pYUz4q6xBOh5tMK67d3n_X1VF1LJhS1mRsGh0JnQ,9960
|
||||
cherrypy/test/test_caching.py,sha256=s6aA_P6mcasaEfPwX50c9A-UqwSiYdzMVp5GFQH08uQ,14386
|
||||
cherrypy/test/test_config.py,sha256=lUxRUCBmDVBo8LK7yi8w5qvFcS3vw4YpFwl66TdRskQ,8836
|
||||
cherrypy/test/test_config_server.py,sha256=D7jLqZawCOh2z6vGU-WjkupiJA0BxPywb8LuILL2JGA,4037
|
||||
cherrypy/test/test_conn.py,sha256=g2e2CCaB_2UiFWVLniVZbk2YNrXqN9_J0M1FymMZ_F8,30744
|
||||
cherrypy/test/test_core.py,sha256=u9lxQENZoTczEzx677_MxyV3i2tmZxl0Wd9yQ-mtIUI,30395
|
||||
cherrypy/test/test_dynamicobjectmapping.py,sha256=0VpMCJq1APhGjqwYoYvx0irFJ4yb3NWqdE5RtK11uDM,12331
|
||||
cherrypy/test/test_encoding.py,sha256=lGUiNKrWQlHma4fV-mjR0vv-xylrw_v1E6j61CT6vBc,17535
|
||||
cherrypy/test/test_etags.py,sha256=mzuKNjFXx67gHoqS_jaGEzjxJ025mzlLgepsW2DYETI,3093
|
||||
cherrypy/test/test_http.py,sha256=Tg1WoG7R03tP8ljwmFUNMG21EVBh6mTBudWmQ0VgfeY,11167
|
||||
cherrypy/test/test_httputil.py,sha256=gA3u7bt1vV2Av3T1I0CaEGEdYV9kGDEPD80al1IbPE8,2412
|
||||
cherrypy/test/test_iterator.py,sha256=siygtCR27EoDoOwyr8Bvk-Gt31dJTLAsS-V3WRYE4PM,5754
|
||||
cherrypy/test/test_json.py,sha256=rVfzyCwSMf79bcZ8aYBA_180FJxcHY9jFT5_0M6-pSc,2860
|
||||
cherrypy/test/test_logging.py,sha256=OtjY-Q87VNPB6rRVDKn0tOScWkFpcSEkZZa4rRmyT4o,9155
|
||||
cherrypy/test/test_mime.py,sha256=-6HpcAIGtN56nWRQclxvSKhNLW5e_bYRO6kDtM3DlN8,4538
|
||||
cherrypy/test/test_misc_tools.py,sha256=Ixjq2IAJZ1BTuV-i_AUKw_OsjYru_i9RPm1gIWyWt_E,7094
|
||||
cherrypy/test/test_native.py,sha256=rtow-ShYRkd2oEBtDksU6e06_L0BvZToFJFngeeGF34,971
|
||||
cherrypy/test/test_objectmapping.py,sha256=KTMqAizhWBGCgDFp1n8msno4ylYPnmvWZ1cWHzzUgO0,14504
|
||||
cherrypy/test/test_params.py,sha256=p4DfugiKWxF9nPX5Gs7arGhUmpx_eeZhWyS5yCXCUj4,1862
|
||||
cherrypy/test/test_plugins.py,sha256=afO6r6XOLYaWPO-Crbei1W0xe_VDA1ippMkCtuo_ypc,334
|
||||
cherrypy/test/test_proxy.py,sha256=XPdi3O_izRLtvu3UTJE-WTBVmn4DEng7irrfUD69srU,5630
|
||||
cherrypy/test/test_refleaks.py,sha256=HK55E9JtRFc28FhnnTLV9DrM1k82ZmPEVdHYyp525K0,1555
|
||||
cherrypy/test/test_request_obj.py,sha256=C3dOHeeqVGxXQjAhDS3Q_7dIzfpyR4WFo6eZ61W8vKI,37404
|
||||
cherrypy/test/test_routes.py,sha256=m0MvSqurFk42PuSp5vF8ue40-vnhPNwC2EGTqkDozo4,2583
|
||||
cherrypy/test/test_session.py,sha256=Fxkm1kuth5tRb6FKiToFLETDmFwnQeVQKmDYLdo6pU0,17949
|
||||
cherrypy/test/test_sessionauthenticate.py,sha256=zhVUpN3FWPaZbMKQcTrDQiaI-RXjlwrJi7ssqbzhmU8,2013
|
||||
cherrypy/test/test_states.py,sha256=2E9S0L_JvJUtVBoJLOCpLgAICoOvE4HpFp3EP0gCLy8,16744
|
||||
cherrypy/test/test_static.py,sha256=4VQpLumcxWGNFwaLuCgOS11pyla1gpGgQNlKhMOopws,16702
|
||||
cherrypy/test/test_tools.py,sha256=cQELdifYvLfwJPg_0JoGQwyFvsgbuFXIJLUd8ejrgZA,17851
|
||||
cherrypy/test/test_tutorials.py,sha256=IszX5SdX3Vlnoq45b9vkdrAIWgc37um4DERFVpP6KcI,6964
|
||||
cherrypy/test/test_virtualhost.py,sha256=ap_e1gM7PERVN4mU70zc5RD1pVoSdN-te-B_uIAlV8g,4021
|
||||
cherrypy/test/test_wsgi_ns.py,sha256=PuoUe2EUwZk5z0yLIw9NdRkd_P4VKU7Cckj8n0QKSJo,2812
|
||||
cherrypy/test/test_wsgi_unix_socket.py,sha256=8guEkMHcOBhOGXYBfAThqCzpIBmUTNpqsilOpaRgD2c,2228
|
||||
cherrypy/test/test_wsgi_vhost.py,sha256=4uZ8_luFHiQJ6uxQeHFJtjemug8UiPcKmnwwclj0dkw,1034
|
||||
cherrypy/test/test_wsgiapps.py,sha256=1SdQGuWVcVCTiSvizDdIOekuvQLCybRXUKF2dpV2NTs,3997
|
||||
cherrypy/test/test_xmlrpc.py,sha256=DQfgzjIMcQP_gOi8el1QQ5dkfPaRbyZ6CDdPL9ZIgWo,4584
|
||||
cherrypy/test/webtest.py,sha256=uRwMw_why3KeXGZXdHE7-GfJag4ziL9KZmDGx4Q7Jbg,262
|
||||
cherrypy/tutorial/README.rst,sha256=v77BbhuiK44TxqeYPk3PwqV09Dg5AKWFdp7re04KdEo,617
|
||||
cherrypy/tutorial/__init__.py,sha256=cmLXfvQI0L6trCXwDzR0WE1bu4JZYt301HJRNhjZOBc,85
|
||||
cherrypy/tutorial/__pycache__/__init__.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut01_helloworld.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut02_expose_methods.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut03_get_and_post.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut04_complex_site.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut05_derived_objects.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut06_default_method.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut07_sessions.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut08_generators_and_yield.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut09_files.cpython-310.pyc,,
|
||||
cherrypy/tutorial/__pycache__/tut10_http_errors.cpython-310.pyc,,
|
||||
cherrypy/tutorial/custom_error.html,sha256=9cMEb83zwct9i-fJlyl7yvBSNexF7yEIWOoxH8lpllQ,404
|
||||
cherrypy/tutorial/pdf_file.pdf,sha256=-WuAfJ9i1vbUT9EcKKvUaNGfbPQsNeFx-qsB4gsxWUg,11961
|
||||
cherrypy/tutorial/tut01_helloworld.py,sha256=Au6IDz0Kd1XMPzb-vJL-1y_BWz0GgrljOtqOpzDZeOk,1015
|
||||
cherrypy/tutorial/tut02_expose_methods.py,sha256=ikz6QOGLknEZm0k-f9BR18deItQ3UO8yYg1NW13kB8g,801
|
||||
cherrypy/tutorial/tut03_get_and_post.py,sha256=bY_cTha4zIkokv585yioQkM-S2a7GfetTE3ovl3-7cw,1587
|
||||
cherrypy/tutorial/tut04_complex_site.py,sha256=PCxyUVKG-L_YUgt1D5Q5t0_IUiGXbcGyf_L00K8cH8c,2948
|
||||
cherrypy/tutorial/tut05_derived_objects.py,sha256=u0LBnUTW8DnexEAtowhJ0sjeAqp6rS065TM2QXfDY1I,2141
|
||||
cherrypy/tutorial/tut06_default_method.py,sha256=3Wx34fL_4P3M_dhu6RQdpXQeSriIJpSsy72IqpNQ6ns,2264
|
||||
cherrypy/tutorial/tut07_sessions.py,sha256=fQo-v_ol5CjXiq4vdsm7Dh1us6DVDzbqAIouRkmLeR8,1228
|
||||
cherrypy/tutorial/tut08_generators_and_yield.py,sha256=m5GfOtNDoGxMd1rw5tCRg3o9cTyt-e1gR-ALoLRLiQw,1288
|
||||
cherrypy/tutorial/tut09_files.py,sha256=qcelN09_k62zWVC-daId5I4i-fw6TWA4WddUbG4j8B8,3463
|
||||
cherrypy/tutorial/tut10_http_errors.py,sha256=6GllO8SI-6Xs6R8hRwHe7jUzGxYJxLlxwxgaOJC9i8Y,2706
|
||||
cherrypy/tutorial/tutorial.conf,sha256=9ENgfRDyopHuignr_aHeMaWoC562xThbmlgF6zg4oEE,96
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
cherryd = cherrypy.__main__:run
|
||||
@ -0,0 +1 @@
|
||||
cherrypy
|
||||
Binary file not shown.
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,242 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: annotated-types
|
||||
Version: 0.6.0
|
||||
Summary: Reusable constraint types to use with typing.Annotated
|
||||
Author-email: Samuel Colvin <s@muelcolvin.com>, Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Zac Hatfield-Dodds <zac@zhd.dev>
|
||||
License-File: LICENSE
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Environment :: MacOS X
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: Information Technology
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: Unix
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# annotated-types
|
||||
|
||||
[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
|
||||
[](https://pypi.python.org/pypi/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
|
||||
|
||||
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
|
||||
adding context-specific metadata to existing types, and specifies that
|
||||
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
|
||||
logic for `x`.
|
||||
|
||||
This package provides metadata objects which can be used to represent common
|
||||
constraints such as upper and lower bounds on scalar values and collection sizes,
|
||||
a `Predicate` marker for runtime checks, and
|
||||
descriptions of how we intend these metadata to be interpreted. In some cases,
|
||||
we also note alternative representations which do not require this package.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install annotated-types
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from annotated_types import Gt, Len, Predicate
|
||||
|
||||
class MyClass:
|
||||
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
|
||||
# Invalid: 17, 18, "19", 19.0, ...
|
||||
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
|
||||
# Invalid: 4, 8, -2, 5.0, "prime", ...
|
||||
|
||||
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
|
||||
# Invalid: (1, 2), ["abc"], [0] * 20
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
_While `annotated-types` avoids runtime checks for performance, users should not
|
||||
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
|
||||
Downstream implementors may choose to raise an error, emit a warning, silently ignore
|
||||
a metadata item, etc., if the metadata objects described below are used with an
|
||||
incompatible type - or for any other reason!_
|
||||
|
||||
### Gt, Ge, Lt, Le
|
||||
|
||||
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
|
||||
dates, times, strings, sets, etc. Note that the boundary value need not be of the
|
||||
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
|
||||
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
|
||||
|
||||
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
|
||||
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
|
||||
the `annotated-types` package.
|
||||
|
||||
To be explicit, these types have the following meanings:
|
||||
|
||||
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
|
||||
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
|
||||
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
|
||||
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
|
||||
|
||||
### Interval
|
||||
|
||||
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
|
||||
metadata object. `None` attributes should be ignored, and non-`None` attributes
|
||||
treated as per the single bounds above.
|
||||
|
||||
### MultipleOf
|
||||
|
||||
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying `value % multiple_of == 0`, or
|
||||
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
|
||||
where `int(value / multiple_of) == value / multiple_of`.
|
||||
|
||||
We encourage users to be aware of these two common interpretations and their
|
||||
distinct behaviours, especially since very large or non-integer numbers make
|
||||
it easy to cause silent data corruption due to floating-point imprecision.
|
||||
|
||||
We encourage libraries to carefully document which interpretation they implement.
|
||||
|
||||
### MinLen, MaxLen, Len
|
||||
|
||||
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
|
||||
|
||||
As well as `Len()` which can optionally include upper and lower bounds, we also
|
||||
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
|
||||
and `Len(max_length=y)` respectively.
|
||||
|
||||
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
|
||||
|
||||
Examples of usage:
|
||||
|
||||
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
|
||||
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
|
||||
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
|
||||
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
|
||||
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
|
||||
|
||||
#### Changed in v0.4.0
|
||||
|
||||
* `min_inclusive` has been renamed to `min_length`, no change in meaning
|
||||
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
|
||||
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
|
||||
meaning of the upper bound in slices vs. `Len`
|
||||
|
||||
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
|
||||
|
||||
### Timezone
|
||||
|
||||
`Timezone` can be used with a `datetime` or a `time` to express which timezones
|
||||
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
|
||||
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
|
||||
expresses that any timezone-aware datetime is allowed. You may also pass a specific
|
||||
timezone string or `timezone` object such as `Timezone(timezone.utc)` or
|
||||
`Timezone("Africa/Abidjan")` to express that you only allow a specific timezone,
|
||||
though we note that this is often a symptom of fragile design.
|
||||
|
||||
### Predicate
|
||||
|
||||
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
|
||||
Users should prefer the statically inspectable metadata above, but if you need
|
||||
the full power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
We provide a few predefined predicates for common string constraints:
|
||||
|
||||
* `IsLower = Predicate(str.islower)`
|
||||
* `IsUpper = Predicate(str.isupper)`
|
||||
* `IsDigit = Predicate(str.isdigit)`
|
||||
* `IsFinite = Predicate(math.isfinite)`
|
||||
* `IsNotFinite = Predicate(Not(math.isfinite))`
|
||||
* `IsNan = Predicate(math.isnan)`
|
||||
* `IsNotNan = Predicate(Not(math.isnan))`
|
||||
* `IsInfinite = Predicate(math.isinf)`
|
||||
* `IsNotInfinite = Predicate(Not(math.isinf))`
|
||||
|
||||
Some libraries might have special logic to handle known or understandable predicates,
|
||||
for example by checking for `str.isdigit` and using its presence to both call custom
|
||||
logic to enforce digit-only strings, and customise some generated external schema.
|
||||
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
|
||||
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
|
||||
|
||||
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propogate or discard the resulting
|
||||
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
|
||||
exception. We encourage libraries to document the behaviour they choose.
|
||||
|
||||
### Doc
|
||||
|
||||
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
|
||||
|
||||
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
|
||||
|
||||
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
|
||||
|
||||
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
|
||||
|
||||
### Integrating downstream types with `GroupedMetadata`
|
||||
|
||||
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
|
||||
This can help reduce verbosity and cognitive overhead for users.
|
||||
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator
|
||||
from annotated_types import GroupedMetadata, Ge
|
||||
|
||||
@dataclass
|
||||
class Field(GroupedMetadata):
|
||||
ge: int | None = None
|
||||
description: str | None = None
|
||||
|
||||
def __iter__(self) -> Iterator[object]:
|
||||
# Iterating over a GroupedMetadata object should yield annotated-types
|
||||
# constraint metadata objects which describe it as fully as possible,
|
||||
# and may include other unknown objects too.
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.description is not None:
|
||||
yield Description(self.description)
|
||||
```
|
||||
|
||||
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
|
||||
|
||||
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
|
||||
|
||||
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
|
||||
|
||||
### Consuming metadata
|
||||
|
||||
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
|
||||
|
||||
It is up to the implementer to determine how this metadata is used.
|
||||
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
|
||||
|
||||
## Design & History
|
||||
|
||||
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
|
||||
and Hypothesis, with the goal of making it as easy as possible for end-users to
|
||||
provide more informative annotations for use by runtime libraries.
|
||||
|
||||
It is deliberately minimal, and following PEP-593 allows considerable downstream
|
||||
discretion in what (if anything!) they choose to support. Nonetheless, we expect
|
||||
that staying simple and covering _only_ the most common use-cases will give users
|
||||
and maintainers the best experience we can. If you'd like more constraints for your
|
||||
types - follow our lead, by defining them and documenting them downstream!
|
||||
@ -0,0 +1,10 @@
|
||||
annotated_types-0.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
annotated_types-0.6.0.dist-info/METADATA,sha256=78YeruT3b_doh8TbsK7qcbqY4cvCMz_QQRCGUbyAo6M,12879
|
||||
annotated_types-0.6.0.dist-info/RECORD,,
|
||||
annotated_types-0.6.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
|
||||
annotated_types-0.6.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
|
||||
annotated_types/__init__.py,sha256=txLoRt8iSiAc-Su37esWpMI0wFjN92V7gE5HdFIeeKI,12151
|
||||
annotated_types/__pycache__/__init__.cpython-310.pyc,,
|
||||
annotated_types/__pycache__/test_cases.cpython-310.pyc,,
|
||||
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
annotated_types/test_cases.py,sha256=LfFyURZwr7X3NVfoCrSXSMMxTxJD4o8Xz-Y8qbmY7JU,6327
|
||||
@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.18.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 the contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,396 @@
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import timezone
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
else:
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated, Literal
|
||||
else:
|
||||
from typing import Annotated, Literal
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
EllipsisType = type(Ellipsis)
|
||||
KW_ONLY = {}
|
||||
SLOTS = {}
|
||||
else:
|
||||
from types import EllipsisType
|
||||
|
||||
KW_ONLY = {"kw_only": True}
|
||||
SLOTS = {"slots": True}
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseMetadata',
|
||||
'GroupedMetadata',
|
||||
'Gt',
|
||||
'Ge',
|
||||
'Lt',
|
||||
'Le',
|
||||
'Interval',
|
||||
'MultipleOf',
|
||||
'MinLen',
|
||||
'MaxLen',
|
||||
'Len',
|
||||
'Timezone',
|
||||
'Predicate',
|
||||
'LowerCase',
|
||||
'UpperCase',
|
||||
'IsDigits',
|
||||
'IsFinite',
|
||||
'IsNotFinite',
|
||||
'IsNan',
|
||||
'IsNotNan',
|
||||
'IsInfinite',
|
||||
'IsNotInfinite',
|
||||
'doc',
|
||||
'DocInfo',
|
||||
'__version__',
|
||||
)
|
||||
|
||||
__version__ = '0.6.0'
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# arguments that start with __ are considered
|
||||
# positional only
|
||||
# see https://peps.python.org/pep-0484/#positional-only-arguments
|
||||
|
||||
|
||||
class SupportsGt(Protocol):
|
||||
def __gt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsGe(Protocol):
|
||||
def __ge__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLt(Protocol):
|
||||
def __lt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLe(Protocol):
|
||||
def __le__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsMod(Protocol):
|
||||
def __mod__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class SupportsDiv(Protocol):
|
||||
def __div__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class BaseMetadata:
|
||||
"""Base class for all metadata.
|
||||
|
||||
This exists mainly so that implementers
|
||||
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Gt(BaseMetadata):
|
||||
"""Gt(gt=x) implies that the value must be greater than x.
|
||||
|
||||
It can be used with any type that supports the ``>`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
gt: SupportsGt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Ge(BaseMetadata):
|
||||
"""Ge(ge=x) implies that the value must be greater than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``>=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
ge: SupportsGe
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Lt(BaseMetadata):
|
||||
"""Lt(lt=x) implies that the value must be less than x.
|
||||
|
||||
It can be used with any type that supports the ``<`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
lt: SupportsLt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Le(BaseMetadata):
|
||||
"""Le(le=x) implies that the value must be less than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``<=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
le: SupportsLe
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GroupedMetadata(Protocol):
|
||||
"""A grouping of multiple BaseMetadata objects.
|
||||
|
||||
`GroupedMetadata` on its own is not metadata and has no meaning.
|
||||
All it the the constraint and metadata should be fully expressable
|
||||
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
|
||||
|
||||
Concrete implementations should override `GroupedMetadata.__iter__()`
|
||||
to add their own metadata.
|
||||
For example:
|
||||
|
||||
>>> @dataclass
|
||||
>>> class Field(GroupedMetadata):
|
||||
>>> gt: float | None = None
|
||||
>>> description: str | None = None
|
||||
...
|
||||
>>> def __iter__(self) -> Iterable[BaseMetadata]:
|
||||
>>> if self.gt is not None:
|
||||
>>> yield Gt(self.gt)
|
||||
>>> if self.description is not None:
|
||||
>>> yield Description(self.gt)
|
||||
|
||||
Also see the implementation of `Interval` below for an example.
|
||||
|
||||
Parsers should recognize this and unpack it so that it can be used
|
||||
both with and without unpacking:
|
||||
|
||||
- `Annotated[int, Field(...)]` (parser must unpack Field)
|
||||
- `Annotated[int, *Field(...)]` (PEP-646)
|
||||
""" # noqa: trailing-whitespace
|
||||
|
||||
@property
|
||||
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
...
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__slots__ = () # allow subclasses to use slots
|
||||
|
||||
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
|
||||
# Basic ABC like functionality without the complexity of an ABC
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
if cls.__iter__ is GroupedMetadata.__iter__:
|
||||
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]: # noqa: F811
|
||||
raise NotImplementedError # more helpful than "None has no attribute..." type errors
|
||||
|
||||
|
||||
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
|
||||
class Interval(GroupedMetadata):
|
||||
"""Interval can express inclusive or exclusive bounds with a single object.
|
||||
|
||||
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
|
||||
are interpreted the same way as the single-bound constraints.
|
||||
"""
|
||||
|
||||
gt: Union[SupportsGt, None] = None
|
||||
ge: Union[SupportsGe, None] = None
|
||||
lt: Union[SupportsLt, None] = None
|
||||
le: Union[SupportsLe, None] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack an Interval into zero or more single-bounds."""
|
||||
if self.gt is not None:
|
||||
yield Gt(self.gt)
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.lt is not None:
|
||||
yield Lt(self.lt)
|
||||
if self.le is not None:
|
||||
yield Le(self.le)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MultipleOf(BaseMetadata):
|
||||
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying ``value % multiple_of == 0``, or
|
||||
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
|
||||
|
||||
We encourage users to be aware of these two common interpretations,
|
||||
and libraries to carefully document which they implement.
|
||||
"""
|
||||
|
||||
multiple_of: Union[SupportsDiv, SupportsMod]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MinLen(BaseMetadata):
|
||||
"""
|
||||
MinLen() implies minimum inclusive length,
|
||||
e.g. ``len(value) >= min_length``.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MaxLen(BaseMetadata):
|
||||
"""
|
||||
MaxLen() implies maximum inclusive length,
|
||||
e.g. ``len(value) <= max_length``.
|
||||
"""
|
||||
|
||||
max_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Len(GroupedMetadata):
|
||||
"""
|
||||
Len() implies that ``min_length <= len(value) <= max_length``.
|
||||
|
||||
Upper bound may be omitted or ``None`` to indicate no upper length bound.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)] = 0
|
||||
max_length: Optional[Annotated[int, Ge(0)]] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack a Len into zone or more single-bounds."""
|
||||
if self.min_length > 0:
|
||||
yield MinLen(self.min_length)
|
||||
if self.max_length is not None:
|
||||
yield MaxLen(self.max_length)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Timezone(BaseMetadata):
|
||||
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
|
||||
|
||||
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
|
||||
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
|
||||
tz-aware but any timezone is allowed.
|
||||
|
||||
You may also pass a specific timezone string or timezone object such as
|
||||
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
|
||||
you only allow a specific timezone, though we note that this is often
|
||||
a symptom of poor design.
|
||||
"""
|
||||
|
||||
tz: Union[str, timezone, EllipsisType, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Predicate(BaseMetadata):
|
||||
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
|
||||
|
||||
Users should prefer statically inspectable metadata, but if you need the full
|
||||
power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
We provide a few predefined predicates for common string constraints:
|
||||
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
|
||||
``IsDigit = Predicate(str.isdigit)``. Users are encouraged to use methods which
|
||||
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
|
||||
|
||||
Some libraries might have special logic to handle certain predicates, e.g. by
|
||||
checking for `str.isdigit` and using its presence to both call custom logic to
|
||||
enforce digit-only strings, and customise some generated external schema.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propogate or discard the resulting exception.
|
||||
"""
|
||||
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Not:
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __call__(self, __v: Any) -> bool:
|
||||
return not self.func(__v)
|
||||
|
||||
|
||||
_StrType = TypeVar("_StrType", bound=str)
|
||||
|
||||
LowerCase = Annotated[_StrType, Predicate(str.islower)]
|
||||
"""
|
||||
Return True if the string is a lowercase string, False otherwise.
|
||||
|
||||
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
|
||||
"""
|
||||
Return True if the string is an uppercase string, False otherwise.
|
||||
|
||||
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
IsDigits = Annotated[_StrType, Predicate(str.isdigit)]
|
||||
"""
|
||||
Return True if the string is a digit string, False otherwise.
|
||||
|
||||
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
|
||||
""" # noqa: E501
|
||||
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
|
||||
"""
|
||||
Return True if all characters in the string are ASCII, False otherwise.
|
||||
|
||||
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
|
||||
"""
|
||||
|
||||
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
|
||||
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
|
||||
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
|
||||
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
|
||||
"""Return True if x is one of infinity or NaN, and False otherwise"""
|
||||
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
|
||||
"""Return True if x is a NaN (not a number), and False otherwise."""
|
||||
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
|
||||
"""Return True if x is anything but NaN (not a number), and False otherwise."""
|
||||
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
|
||||
"""Return True if x is a positive or negative infinity, and False otherwise."""
|
||||
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
|
||||
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
|
||||
|
||||
try:
|
||||
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
|
||||
except ImportError:
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class DocInfo: # type: ignore [no-redef]
|
||||
""" "
|
||||
The return value of doc(), mainly to be used by tools that want to extract the
|
||||
Annotated documentation at runtime.
|
||||
"""
|
||||
|
||||
documentation: str
|
||||
"""The documentation string passed to doc()."""
|
||||
|
||||
def doc(
|
||||
documentation: str,
|
||||
) -> DocInfo:
|
||||
"""
|
||||
Add documentation to a type annotation inside of Annotated.
|
||||
|
||||
For example:
|
||||
|
||||
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
|
||||
"""
|
||||
return DocInfo(documentation)
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,147 @@
|
||||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated
|
||||
else:
|
||||
from typing import Annotated
|
||||
|
||||
import annotated_types as at
|
||||
|
||||
|
||||
class Case(NamedTuple):
|
||||
"""
|
||||
A test case for `annotated_types`.
|
||||
"""
|
||||
|
||||
annotation: Any
|
||||
valid_cases: Iterable[Any]
|
||||
invalid_cases: Iterable[Any]
|
||||
|
||||
|
||||
def cases() -> Iterable[Case]:
|
||||
# Gt, Ge, Lt, Le
|
||||
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(date(2000, 1, 1))],
|
||||
[date(2000, 1, 2), date(2000, 1, 3)],
|
||||
[date(2000, 1, 1), date(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(Decimal('1.123'))],
|
||||
[Decimal('1.1231'), Decimal('123')],
|
||||
[Decimal('1.123'), Decimal('0')],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
|
||||
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
|
||||
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
|
||||
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
|
||||
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
# Interval
|
||||
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
|
||||
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
|
||||
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
|
||||
|
||||
# lengths
|
||||
|
||||
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
|
||||
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
|
||||
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
|
||||
|
||||
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
|
||||
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
|
||||
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
|
||||
|
||||
# Timezone
|
||||
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(timezone.utc)],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone('Europe/London')],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
|
||||
# predicate types
|
||||
|
||||
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
|
||||
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
|
||||
yield Case(at.IsDigits[str], ['123'], ['', 'ab', 'a1b2'])
|
||||
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
|
||||
|
||||
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
|
||||
|
||||
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
|
||||
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
|
||||
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
|
||||
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
|
||||
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
|
||||
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
|
||||
|
||||
# check stacked predicates
|
||||
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
|
||||
|
||||
# doc
|
||||
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
|
||||
|
||||
# custom GroupedMetadata
|
||||
class MyCustomGroupedMetadata(at.GroupedMetadata):
|
||||
def __iter__(self) -> Iterator[at.Predicate]:
|
||||
yield at.Predicate(lambda x: float(x).is_integer())
|
||||
|
||||
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,166 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
||||
@ -0,0 +1,420 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: autocommand
|
||||
Version: 2.2.2
|
||||
Summary: A library to create a command-line program from a function
|
||||
Home-page: https://github.com/Lucretiel/autocommand
|
||||
Author: Nathan West
|
||||
License: LGPLv3
|
||||
Project-URL: Homepage, https://github.com/Lucretiel/autocommand
|
||||
Project-URL: Bug Tracker, https://github.com/Lucretiel/autocommand/issues
|
||||
Platform: any
|
||||
Classifier: Development Status :: 6 - Mature
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Topic :: Software Development
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
|
||||
[](https://badge.fury.io/py/autocommand)
|
||||
|
||||
# autocommand
|
||||
|
||||
A library to automatically generate and run simple argparse parsers from function signatures.
|
||||
|
||||
## Installation
|
||||
|
||||
Autocommand is installed via pip:
|
||||
|
||||
```
|
||||
$ pip install autocommand
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Autocommand turns a function into a command-line program. It converts the function's parameter signature into command-line arguments, and automatically runs the function if the module was called as `__main__`. In effect, it lets your create a smart main function.
|
||||
|
||||
```python
|
||||
from autocommand import autocommand
|
||||
|
||||
# This program takes exactly one argument and echos it.
|
||||
@autocommand(__name__)
|
||||
def echo(thing):
|
||||
print(thing)
|
||||
```
|
||||
|
||||
```
|
||||
$ python echo.py hello
|
||||
hello
|
||||
$ python echo.py -h
|
||||
usage: echo [-h] thing
|
||||
|
||||
positional arguments:
|
||||
thing
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
$ python echo.py hello world # too many arguments
|
||||
usage: echo.py [-h] thing
|
||||
echo.py: error: unrecognized arguments: world
|
||||
```
|
||||
|
||||
As you can see, autocommand converts the signature of the function into an argument spec. When you run the file as a program, autocommand collects the command-line arguments and turns them into function arguments. The function is executed with these arguments, and then the program exits with the return value of the function, via `sys.exit`. Autocommand also automatically creates a usage message, which can be invoked with `-h` or `--help`, and automatically prints an error message when provided with invalid arguments.
|
||||
|
||||
### Types
|
||||
|
||||
You can use a type annotation to give an argument a type. Any type (or in fact any callable) that returns an object when given a string argument can be used, though there are a few special cases that are described later.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def net_client(host, port: int):
|
||||
...
|
||||
```
|
||||
|
||||
Autocommand will catch `TypeErrors` raised by the type during argument parsing, so you can supply a callable and do some basic argument validation as well.
|
||||
|
||||
### Trailing Arguments
|
||||
|
||||
You can add a `*args` parameter to your function to give it trailing arguments. The command will collect 0 or more trailing arguments and supply them to `args` as a tuple. If a type annotation is supplied, the type is applied to each argument.
|
||||
|
||||
```python
|
||||
# Write the contents of each file, one by one
|
||||
@autocommand(__name__)
|
||||
def cat(*files):
|
||||
for filename in files:
|
||||
with open(filename) as file:
|
||||
for line in file:
|
||||
print(line.rstrip())
|
||||
```
|
||||
|
||||
```
|
||||
$ python cat.py -h
|
||||
usage: ipython [-h] [file [file ...]]
|
||||
|
||||
positional arguments:
|
||||
file
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
To create `--option` switches, just assign a default. Autocommand will automatically create `--long` and `-s`hort switches.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def do_with_config(argument, config='~/foo.conf'):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: example.py [-h] [-c CONFIG] argument
|
||||
|
||||
positional arguments:
|
||||
argument
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
```
|
||||
|
||||
The option's type is automatically deduced from the default, unless one is explicitly given in an annotation:
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def http_connect(host, port=80):
|
||||
print('{}:{}'.format(host, port))
|
||||
```
|
||||
|
||||
```
|
||||
$ python http.py -h
|
||||
usage: http.py [-h] [-p PORT] host
|
||||
|
||||
positional arguments:
|
||||
host
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PORT, --port PORT
|
||||
$ python http.py localhost
|
||||
localhost:80
|
||||
$ python http.py localhost -p 8080
|
||||
localhost:8080
|
||||
$ python http.py localhost -p blah
|
||||
usage: http.py [-h] [-p PORT] host
|
||||
http.py: error: argument -p/--port: invalid int value: 'blah'
|
||||
```
|
||||
|
||||
#### None
|
||||
|
||||
If an option is given a default value of `None`, it reads in a value as normal, but supplies `None` if the option isn't provided.
|
||||
|
||||
#### Switches
|
||||
|
||||
If an argument is given a default value of `True` or `False`, or
|
||||
given an explicit `bool` type, it becomes an option switch.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def example(verbose=False, quiet=False):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: example.py [-h] [-v] [-q]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
```
|
||||
|
||||
Autocommand attempts to do the "correct thing" in these cases- if the default is `True`, then supplying the switch makes the argument `False`; if the type is `bool` and the default is some other `True` value, then supplying the switch makes the argument `False`, while not supplying the switch makes the argument the default value.
|
||||
|
||||
Autocommand also supports the creation of switch inverters. Pass `add_nos=True` to `autocommand` to enable this.
|
||||
|
||||
```
|
||||
@autocommand(__name__, add_nos=True)
|
||||
def example(verbose=False):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: ipython [-h] [-v] [--no-verbose]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
--no-verbose
|
||||
```
|
||||
|
||||
Using the `--no-` version of a switch will pass the opposite value in as a function argument. If multiple switches are present, the last one takes precedence.
|
||||
|
||||
#### Files
|
||||
|
||||
If the default value is a file object, such as `sys.stdout`, then autocommand just looks for a string, for a file path. It doesn't do any special checking on the string, though (such as checking if the file exists); it's better to let the client decide how to handle errors in this case. Instead, it provides a special context manager called `smart_open`, which behaves exactly like `open` if a filename or other openable type is provided, but also lets you use already open files:
|
||||
|
||||
```python
|
||||
from autocommand import autocommand, smart_open
|
||||
import sys
|
||||
|
||||
# Write the contents of stdin, or a file, to stdout
|
||||
@autocommand(__name__)
|
||||
def write_out(infile=sys.stdin):
|
||||
with smart_open(infile) as f:
|
||||
for line in f:
|
||||
print(line.rstrip())
|
||||
# If a file was opened, it is closed here. If it was just stdin, it is untouched.
|
||||
```
|
||||
|
||||
```
|
||||
$ echo "Hello World!" | python write_out.py | tee hello.txt
|
||||
Hello World!
|
||||
$ python write_out.py --infile hello.txt
|
||||
Hello World!
|
||||
```
|
||||
|
||||
### Descriptions and docstrings
|
||||
|
||||
The `autocommand` decorator accepts `description` and `epilog` kwargs, corresponding to the `description <https://docs.python.org/3/library/argparse.html#description>`_ and `epilog <https://docs.python.org/3/library/argparse.html#epilog>`_ of the `ArgumentParser`. If no description is given, but the decorated function has a docstring, then it is taken as the `description` for the `ArgumentParser`. You can also provide both the description and epilog in the docstring by splitting it into two sections with 4 or more - characters.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def copy(infile=sys.stdin, outfile=sys.stdout):
|
||||
'''
|
||||
Copy an the contents of a file (or stdin) to another file (or stdout)
|
||||
----------
|
||||
Some extra documentation in the epilog
|
||||
'''
|
||||
with smart_open(infile) as istr:
|
||||
with smart_open(outfile, 'w') as ostr:
|
||||
for line in istr:
|
||||
ostr.write(line)
|
||||
```
|
||||
|
||||
```
|
||||
$ python copy.py -h
|
||||
usage: copy.py [-h] [-i INFILE] [-o OUTFILE]
|
||||
|
||||
Copy an the contents of a file (or stdin) to another file (or stdout)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i INFILE, --infile INFILE
|
||||
-o OUTFILE, --outfile OUTFILE
|
||||
|
||||
Some extra documentation in the epilog
|
||||
$ echo "Hello World" | python copy.py --outfile hello.txt
|
||||
$ python copy.py --infile hello.txt --outfile hello2.txt
|
||||
$ python copy.py --infile hello2.txt
|
||||
Hello World
|
||||
```
|
||||
|
||||
### Parameter descriptions
|
||||
|
||||
You can also attach description text to individual parameters in the annotation. To attach both a type and a description, supply them both in any order in a tuple
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def copy_net(
|
||||
infile: 'The name of the file to send',
|
||||
host: 'The host to send the file to',
|
||||
port: (int, 'The port to connect to')):
|
||||
|
||||
'''
|
||||
Copy a file over raw TCP to a remote destination.
|
||||
'''
|
||||
# Left as an exercise to the reader
|
||||
```
|
||||
|
||||
### Decorators and wrappers
|
||||
|
||||
Autocommand automatically follows wrapper chains created by `@functools.wraps`. This means that you can apply other wrapping decorators to your main function, and autocommand will still correctly detect the signature.
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from autocommand import autocommand
|
||||
|
||||
def print_yielded(func):
|
||||
'''
|
||||
Convert a generator into a function that prints all yielded elements
|
||||
'''
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
for thing in func(*args, **kwargs):
|
||||
print(thing)
|
||||
return wrapper
|
||||
|
||||
@autocommand(__name__,
|
||||
description= 'Print all the values from START to STOP, inclusive, in steps of STEP',
|
||||
epilog= 'STOP and STEP default to 1')
|
||||
@print_yielded
|
||||
def seq(stop, start=1, step=1):
|
||||
for i in range(start, stop + 1, step):
|
||||
yield i
|
||||
```
|
||||
|
||||
```
|
||||
$ seq.py -h
|
||||
usage: seq.py [-h] [-s START] [-S STEP] stop
|
||||
|
||||
Print all the values from START to STOP, inclusive, in steps of STEP
|
||||
|
||||
positional arguments:
|
||||
stop
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-s START, --start START
|
||||
-S STEP, --step STEP
|
||||
|
||||
STOP and STEP default to 1
|
||||
```
|
||||
|
||||
Even though autocommand is being applied to the `wrapper` returned by `print_yielded`, it still retreives the signature of the underlying `seq` function to create the argument parsing.
|
||||
|
||||
### Custom Parser
|
||||
|
||||
While autocommand's automatic parser generator is a powerful convenience, it doesn't cover all of the different features that argparse provides. If you need these features, you can provide your own parser as a kwarg to `autocommand`:
|
||||
|
||||
```python
|
||||
from argparse import ArgumentParser
|
||||
from autocommand import autocommand
|
||||
|
||||
parser = ArgumentParser()
|
||||
# autocommand can't do optional positonal parameters
|
||||
parser.add_argument('arg', nargs='?')
|
||||
# or mutually exclusive options
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('-v', '--verbose', action='store_true')
|
||||
group.add_argument('-q', '--quiet', action='store_true')
|
||||
|
||||
@autocommand(__name__, parser=parser)
|
||||
def main(arg, verbose, quiet):
|
||||
print(arg, verbose, quiet)
|
||||
```
|
||||
|
||||
```
|
||||
$ python parser.py -h
|
||||
usage: write_file.py [-h] [-v | -q] [arg]
|
||||
|
||||
positional arguments:
|
||||
arg
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
$ python parser.py
|
||||
None False False
|
||||
$ python parser.py hello
|
||||
hello False False
|
||||
$ python parser.py -v
|
||||
None True False
|
||||
$ python parser.py -q
|
||||
None False True
|
||||
$ python parser.py -vq
|
||||
usage: parser.py [-h] [-v | -q] [arg]
|
||||
parser.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
|
||||
```
|
||||
|
||||
Any parser should work fine, so long as each of the parser's arguments has a corresponding parameter in the decorated main function. The order of parameters doesn't matter, as long as they are all present. Note that when using a custom parser, autocommand doesn't modify the parser or the retrieved arguments. This means that no description/epilog will be added, and the function's type annotations and defaults (if present) will be ignored.
|
||||
|
||||
## Testing and Library use
|
||||
|
||||
The decorated function is only called and exited from if the first argument to `autocommand` is `'__main__'` or `True`. If it is neither of these values, or no argument is given, then a new main function is created by the decorator. This function has the signature `main(argv=None)`, and is intended to be called with arguments as if via `main(sys.argv[1:])`. The function has the attributes `parser` and `main`, which are the generated `ArgumentParser` and the original main function that was decorated. This is to facilitate testing and library use of your main. Calling the function triggers a `parse_args()` with the supplied arguments, and returns the result of the main function. Note that, while it returns instead of calling `sys.exit`, the `parse_args()` function will raise a `SystemExit` in the event of a parsing error or `-h/--help` argument.
|
||||
|
||||
```python
|
||||
@autocommand()
|
||||
def test_prog(arg1, arg2: int, quiet=False, verbose=False):
|
||||
if not quiet:
|
||||
print(arg1, arg2)
|
||||
if verbose:
|
||||
print("LOUD NOISES")
|
||||
|
||||
return 0
|
||||
|
||||
print(test_prog(['-v', 'hello', '80']))
|
||||
```
|
||||
|
||||
```
|
||||
$ python test_prog.py
|
||||
hello 80
|
||||
LOUD NOISES
|
||||
0
|
||||
```
|
||||
|
||||
If the function is called with no arguments, `sys.argv[1:]` is used. This is to allow the autocommand function to be used as a setuptools entry point.
|
||||
|
||||
## Exceptions and limitations
|
||||
|
||||
- There are a few possible exceptions that `autocommand` can raise. All of them derive from `autocommand.AutocommandError`.
|
||||
|
||||
- If an invalid annotation is given (that is, it isn't a `type`, `str`, `(type, str)`, or `(str, type)`, an `AnnotationError` is raised. The `type` may be any callable, as described in the `Types`_ section.
|
||||
- If the function has a `**kwargs` parameter, a `KWargError` is raised.
|
||||
- If, somehow, the function has a positional-only parameter, a `PositionalArgError` is raised. This means that the argument doesn't have a name, which is currently not possible with a plain `def` or `lambda`, though many built-in functions have this kind of parameter.
|
||||
|
||||
- There are a few argparse features that are not supported by autocommand.
|
||||
|
||||
- It isn't possible to have an optional positional argument (as opposed to a `--option`). POSIX thinks this is bad form anyway.
|
||||
- It isn't possible to have mutually exclusive arguments or options
|
||||
- It isn't possible to have subcommands or subparsers, though I'm working on a few solutions involving classes or nested function definitions to allow this.
|
||||
|
||||
## Development
|
||||
|
||||
Autocommand cannot be important from the project root; this is to enforce separation of concerns and prevent accidental importing of `setup.py` or tests. To develop, install the project in editable mode:
|
||||
|
||||
```
|
||||
$ python setup.py develop
|
||||
```
|
||||
|
||||
This will create a link to the source files in the deployment directory, so that any source changes are reflected when it is imported.
|
||||
@ -0,0 +1,18 @@
|
||||
autocommand-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
autocommand-2.2.2.dist-info/LICENSE,sha256=reeNBJgtaZctREqOFKlPh6IzTdOFXMgDSOqOJAqg3y0,7634
|
||||
autocommand-2.2.2.dist-info/METADATA,sha256=OADZuR3O6iBlpu1ieTgzYul6w4uOVrk0P0BO5TGGAJk,15006
|
||||
autocommand-2.2.2.dist-info/RECORD,,
|
||||
autocommand-2.2.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
||||
autocommand-2.2.2.dist-info/top_level.txt,sha256=AzfhgKKS8EdAwWUTSF8mgeVQbXOY9kokHB6kSqwwqu0,12
|
||||
autocommand/__init__.py,sha256=zko5Rnvolvb-UXjCx_2ArPTGBWwUK5QY4LIQIKYR7As,1037
|
||||
autocommand/__pycache__/__init__.cpython-310.pyc,,
|
||||
autocommand/__pycache__/autoasync.cpython-310.pyc,,
|
||||
autocommand/__pycache__/autocommand.cpython-310.pyc,,
|
||||
autocommand/__pycache__/automain.cpython-310.pyc,,
|
||||
autocommand/__pycache__/autoparse.cpython-310.pyc,,
|
||||
autocommand/__pycache__/errors.cpython-310.pyc,,
|
||||
autocommand/autoasync.py,sha256=AMdyrxNS4pqWJfP_xuoOcImOHWD-qT7x06wmKN1Vp-U,5680
|
||||
autocommand/autocommand.py,sha256=hmkEmQ72HtL55gnURVjDOnsfYlGd5lLXbvT4KG496Qw,2505
|
||||
autocommand/automain.py,sha256=A2b8i754Mxc_DjU9WFr6vqYDWlhz0cn8miu8d8EsxV8,2076
|
||||
autocommand/autoparse.py,sha256=WVWmZJPcbzUKXP40raQw_0HD8qPJ2V9VG1eFFmmnFxw,11642
|
||||
autocommand/errors.py,sha256=7aa3roh9Herd6nIKpQHNWEslWE8oq7GiHYVUuRqORnA,886
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.38.4)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1 @@
|
||||
autocommand
|
||||
@ -0,0 +1,27 @@
|
||||
# Copyright 2014-2016 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# flake8 flags all these imports as unused, hence the NOQAs everywhere.
|
||||
|
||||
from .automain import automain # NOQA
|
||||
from .autoparse import autoparse, smart_open # NOQA
|
||||
from .autocommand import autocommand # NOQA
|
||||
|
||||
try:
|
||||
from .autoasync import autoasync # NOQA
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,142 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from asyncio import get_event_loop, iscoroutine
|
||||
from functools import wraps
|
||||
from inspect import signature
|
||||
|
||||
|
||||
async def _run_forever_coro(coro, args, kwargs, loop):
|
||||
'''
|
||||
This helper function launches an async main function that was tagged with
|
||||
forever=True. There are two possibilities:
|
||||
|
||||
- The function is a normal function, which handles initializing the event
|
||||
loop, which is then run forever
|
||||
- The function is a coroutine, which needs to be scheduled in the event
|
||||
loop, which is then run forever
|
||||
- There is also the possibility that the function is a normal function
|
||||
wrapping a coroutine function
|
||||
|
||||
The function is therefore called unconditionally and scheduled in the event
|
||||
loop if the return value is a coroutine object.
|
||||
|
||||
The reason this is a separate function is to make absolutely sure that all
|
||||
the objects created are garbage collected after all is said and done; we
|
||||
do this to ensure that any exceptions raised in the tasks are collected
|
||||
ASAP.
|
||||
'''
|
||||
|
||||
# Personal note: I consider this an antipattern, as it relies on the use of
|
||||
# unowned resources. The setup function dumps some stuff into the event
|
||||
# loop where it just whirls in the ether without a well defined owner or
|
||||
# lifetime. For this reason, there's a good chance I'll remove the
|
||||
# forever=True feature from autoasync at some point in the future.
|
||||
thing = coro(*args, **kwargs)
|
||||
if iscoroutine(thing):
|
||||
await thing
|
||||
|
||||
|
||||
def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
|
||||
'''
|
||||
Convert an asyncio coroutine into a function which, when called, is
|
||||
evaluted in an event loop, and the return value returned. This is intented
|
||||
to make it easy to write entry points into asyncio coroutines, which
|
||||
otherwise need to be explictly evaluted with an event loop's
|
||||
run_until_complete.
|
||||
|
||||
If `loop` is given, it is used as the event loop to run the coro in. If it
|
||||
is None (the default), the loop is retreived using asyncio.get_event_loop.
|
||||
This call is defered until the decorated function is called, so that
|
||||
callers can install custom event loops or event loop policies after
|
||||
@autoasync is applied.
|
||||
|
||||
If `forever` is True, the loop is run forever after the decorated coroutine
|
||||
is finished. Use this for servers created with asyncio.start_server and the
|
||||
like.
|
||||
|
||||
If `pass_loop` is True, the event loop object is passed into the coroutine
|
||||
as the `loop` kwarg when the wrapper function is called. In this case, the
|
||||
wrapper function's __signature__ is updated to remove this parameter, so
|
||||
that autoparse can still be used on it without generating a parameter for
|
||||
`loop`.
|
||||
|
||||
This coroutine can be called with ( @autoasync(...) ) or without
|
||||
( @autoasync ) arguments.
|
||||
|
||||
Examples:
|
||||
|
||||
@autoasync
|
||||
def get_file(host, port):
|
||||
reader, writer = yield from asyncio.open_connection(host, port)
|
||||
data = reader.read()
|
||||
sys.stdout.write(data.decode())
|
||||
|
||||
get_file(host, port)
|
||||
|
||||
@autoasync(forever=True, pass_loop=True)
|
||||
def server(host, port, loop):
|
||||
yield_from loop.create_server(Proto, host, port)
|
||||
|
||||
server('localhost', 8899)
|
||||
|
||||
'''
|
||||
if coro is None:
|
||||
return lambda c: autoasync(
|
||||
c, loop=loop,
|
||||
forever=forever,
|
||||
pass_loop=pass_loop)
|
||||
|
||||
# The old and new signatures are required to correctly bind the loop
|
||||
# parameter in 100% of cases, even if it's a positional parameter.
|
||||
# NOTE: A future release will probably require the loop parameter to be
|
||||
# a kwonly parameter.
|
||||
if pass_loop:
|
||||
old_sig = signature(coro)
|
||||
new_sig = old_sig.replace(parameters=(
|
||||
param for name, param in old_sig.parameters.items()
|
||||
if name != "loop"))
|
||||
|
||||
@wraps(coro)
|
||||
def autoasync_wrapper(*args, **kwargs):
|
||||
# Defer the call to get_event_loop so that, if a custom policy is
|
||||
# installed after the autoasync decorator, it is respected at call time
|
||||
local_loop = get_event_loop() if loop is None else loop
|
||||
|
||||
# Inject the 'loop' argument. We have to use this signature binding to
|
||||
# ensure it's injected in the correct place (positional, keyword, etc)
|
||||
if pass_loop:
|
||||
bound_args = old_sig.bind_partial()
|
||||
bound_args.arguments.update(
|
||||
loop=local_loop,
|
||||
**new_sig.bind(*args, **kwargs).arguments)
|
||||
args, kwargs = bound_args.args, bound_args.kwargs
|
||||
|
||||
if forever:
|
||||
local_loop.create_task(_run_forever_coro(
|
||||
coro, args, kwargs, local_loop
|
||||
))
|
||||
local_loop.run_forever()
|
||||
else:
|
||||
return local_loop.run_until_complete(coro(*args, **kwargs))
|
||||
|
||||
# Attach the updated signature. This allows 'pass_loop' to be used with
|
||||
# autoparse
|
||||
if pass_loop:
|
||||
autoasync_wrapper.__signature__ = new_sig
|
||||
|
||||
return autoasync_wrapper
|
||||
@ -0,0 +1,70 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .autoparse import autoparse
|
||||
from .automain import automain
|
||||
try:
|
||||
from .autoasync import autoasync
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def autocommand(
|
||||
module, *,
|
||||
description=None,
|
||||
epilog=None,
|
||||
add_nos=False,
|
||||
parser=None,
|
||||
loop=None,
|
||||
forever=False,
|
||||
pass_loop=False):
|
||||
|
||||
if callable(module):
|
||||
raise TypeError('autocommand requires a module name argument')
|
||||
|
||||
def autocommand_decorator(func):
|
||||
# Step 1: if requested, run it all in an asyncio event loop. autoasync
|
||||
# patches the __signature__ of the decorated function, so that in the
|
||||
# event that pass_loop is True, the `loop` parameter of the original
|
||||
# function will *not* be interpreted as a command-line argument by
|
||||
# autoparse
|
||||
if loop is not None or forever or pass_loop:
|
||||
func = autoasync(
|
||||
func,
|
||||
loop=None if loop is True else loop,
|
||||
pass_loop=pass_loop,
|
||||
forever=forever)
|
||||
|
||||
# Step 2: create parser. We do this second so that the arguments are
|
||||
# parsed and passed *before* entering the asyncio event loop, if it
|
||||
# exists. This simplifies the stack trace and ensures errors are
|
||||
# reported earlier. It also ensures that errors raised during parsing &
|
||||
# passing are still raised if `forever` is True.
|
||||
func = autoparse(
|
||||
func,
|
||||
description=description,
|
||||
epilog=epilog,
|
||||
add_nos=add_nos,
|
||||
parser=parser)
|
||||
|
||||
# Step 3: call the function automatically if __name__ == '__main__' (or
|
||||
# if True was provided)
|
||||
func = automain(module)(func)
|
||||
|
||||
return func
|
||||
|
||||
return autocommand_decorator
|
||||
@ -0,0 +1,59 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from .errors import AutocommandError
|
||||
|
||||
|
||||
class AutomainRequiresModuleError(AutocommandError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
def automain(module, *, args=(), kwargs=None):
|
||||
'''
|
||||
This decorator automatically invokes a function if the module is being run
|
||||
as the "__main__" module. Optionally, provide args or kwargs with which to
|
||||
call the function. If `module` is "__main__", the function is called, and
|
||||
the program is `sys.exit`ed with the return value. You can also pass `True`
|
||||
to cause the function to be called unconditionally. If the function is not
|
||||
called, it is returned unchanged by the decorator.
|
||||
|
||||
Usage:
|
||||
|
||||
@automain(__name__) # Pass __name__ to check __name__=="__main__"
|
||||
def main():
|
||||
...
|
||||
|
||||
If __name__ is "__main__" here, the main function is called, and then
|
||||
sys.exit called with the return value.
|
||||
'''
|
||||
|
||||
# Check that @automain(...) was called, rather than @automain
|
||||
if callable(module):
|
||||
raise AutomainRequiresModuleError(module)
|
||||
|
||||
if module == '__main__' or module is True:
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
# Use a function definition instead of a lambda for a neater traceback
|
||||
def automain_decorator(main):
|
||||
sys.exit(main(*args, **kwargs))
|
||||
|
||||
return automain_decorator
|
||||
else:
|
||||
return lambda main: main
|
||||
@ -0,0 +1,333 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from re import compile as compile_regex
|
||||
from inspect import signature, getdoc, Parameter
|
||||
from argparse import ArgumentParser
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from io import IOBase
|
||||
from autocommand.errors import AutocommandError
|
||||
|
||||
|
||||
_empty = Parameter.empty
|
||||
|
||||
|
||||
class AnnotationError(AutocommandError):
|
||||
'''Annotation error: annotation must be a string, type, or tuple of both'''
|
||||
|
||||
|
||||
class PositionalArgError(AutocommandError):
|
||||
'''
|
||||
Postional Arg Error: autocommand can't handle postional-only parameters
|
||||
'''
|
||||
|
||||
|
||||
class KWArgError(AutocommandError):
|
||||
'''kwarg Error: autocommand can't handle a **kwargs parameter'''
|
||||
|
||||
|
||||
class DocstringError(AutocommandError):
|
||||
'''Docstring error'''
|
||||
|
||||
|
||||
class TooManySplitsError(DocstringError):
|
||||
'''
|
||||
The docstring had too many ---- section splits. Currently we only support
|
||||
using up to a single split, to split the docstring into description and
|
||||
epilog parts.
|
||||
'''
|
||||
|
||||
|
||||
def _get_type_description(annotation):
|
||||
'''
|
||||
Given an annotation, return the (type, description) for the parameter.
|
||||
If you provide an annotation that is somehow both a string and a callable,
|
||||
the behavior is undefined.
|
||||
'''
|
||||
if annotation is _empty:
|
||||
return None, None
|
||||
elif callable(annotation):
|
||||
return annotation, None
|
||||
elif isinstance(annotation, str):
|
||||
return None, annotation
|
||||
elif isinstance(annotation, tuple):
|
||||
try:
|
||||
arg1, arg2 = annotation
|
||||
except ValueError as e:
|
||||
raise AnnotationError(annotation) from e
|
||||
else:
|
||||
if callable(arg1) and isinstance(arg2, str):
|
||||
return arg1, arg2
|
||||
elif isinstance(arg1, str) and callable(arg2):
|
||||
return arg2, arg1
|
||||
|
||||
raise AnnotationError(annotation)
|
||||
|
||||
|
||||
def _add_arguments(param, parser, used_char_args, add_nos):
|
||||
'''
|
||||
Add the argument(s) to an ArgumentParser (using add_argument) for a given
|
||||
parameter. used_char_args is the set of -short options currently already in
|
||||
use, and is updated (if necessary) by this function. If add_nos is True,
|
||||
this will also add an inverse switch for all boolean options. For
|
||||
instance, for the boolean parameter "verbose", this will create --verbose
|
||||
and --no-verbose.
|
||||
'''
|
||||
|
||||
# Impl note: This function is kept separate from make_parser because it's
|
||||
# already very long and I wanted to separate out as much as possible into
|
||||
# its own call scope, to prevent even the possibility of suble mutation
|
||||
# bugs.
|
||||
if param.kind is param.POSITIONAL_ONLY:
|
||||
raise PositionalArgError(param)
|
||||
elif param.kind is param.VAR_KEYWORD:
|
||||
raise KWArgError(param)
|
||||
|
||||
# These are the kwargs for the add_argument function.
|
||||
arg_spec = {}
|
||||
is_option = False
|
||||
|
||||
# Get the type and default from the annotation.
|
||||
arg_type, description = _get_type_description(param.annotation)
|
||||
|
||||
# Get the default value
|
||||
default = param.default
|
||||
|
||||
# If there is no explicit type, and the default is present and not None,
|
||||
# infer the type from the default.
|
||||
if arg_type is None and default not in {_empty, None}:
|
||||
arg_type = type(default)
|
||||
|
||||
# Add default. The presence of a default means this is an option, not an
|
||||
# argument.
|
||||
if default is not _empty:
|
||||
arg_spec['default'] = default
|
||||
is_option = True
|
||||
|
||||
# Add the type
|
||||
if arg_type is not None:
|
||||
# Special case for bool: make it just a --switch
|
||||
if arg_type is bool:
|
||||
if not default or default is _empty:
|
||||
arg_spec['action'] = 'store_true'
|
||||
else:
|
||||
arg_spec['action'] = 'store_false'
|
||||
|
||||
# Switches are always options
|
||||
is_option = True
|
||||
|
||||
# Special case for file types: make it a string type, for filename
|
||||
elif isinstance(default, IOBase):
|
||||
arg_spec['type'] = str
|
||||
|
||||
# TODO: special case for list type.
|
||||
# - How to specificy type of list members?
|
||||
# - param: [int]
|
||||
# - param: int =[]
|
||||
# - action='append' vs nargs='*'
|
||||
|
||||
else:
|
||||
arg_spec['type'] = arg_type
|
||||
|
||||
# nargs: if the signature includes *args, collect them as trailing CLI
|
||||
# arguments in a list. *args can't have a default value, so it can never be
|
||||
# an option.
|
||||
if param.kind is param.VAR_POSITIONAL:
|
||||
# TODO: consider depluralizing metavar/name here.
|
||||
arg_spec['nargs'] = '*'
|
||||
|
||||
# Add description.
|
||||
if description is not None:
|
||||
arg_spec['help'] = description
|
||||
|
||||
# Get the --flags
|
||||
flags = []
|
||||
name = param.name
|
||||
|
||||
if is_option:
|
||||
# Add the first letter as a -short option.
|
||||
for letter in name[0], name[0].swapcase():
|
||||
if letter not in used_char_args:
|
||||
used_char_args.add(letter)
|
||||
flags.append('-{}'.format(letter))
|
||||
break
|
||||
|
||||
# If the parameter is a --long option, or is a -short option that
|
||||
# somehow failed to get a flag, add it.
|
||||
if len(name) > 1 or not flags:
|
||||
flags.append('--{}'.format(name))
|
||||
|
||||
arg_spec['dest'] = name
|
||||
else:
|
||||
flags.append(name)
|
||||
|
||||
parser.add_argument(*flags, **arg_spec)
|
||||
|
||||
# Create the --no- version for boolean switches
|
||||
if add_nos and arg_type is bool:
|
||||
parser.add_argument(
|
||||
'--no-{}'.format(name),
|
||||
action='store_const',
|
||||
dest=name,
|
||||
const=default if default is not _empty else False)
|
||||
|
||||
|
||||
def make_parser(func_sig, description, epilog, add_nos):
|
||||
'''
|
||||
Given the signature of a function, create an ArgumentParser
|
||||
'''
|
||||
parser = ArgumentParser(description=description, epilog=epilog)
|
||||
|
||||
used_char_args = {'h'}
|
||||
|
||||
# Arange the params so that single-character arguments are first. This
|
||||
# esnures they don't have to get --long versions. sorted is stable, so the
|
||||
# parameters will otherwise still be in relative order.
|
||||
params = sorted(
|
||||
func_sig.parameters.values(),
|
||||
key=lambda param: len(param.name) > 1)
|
||||
|
||||
for param in params:
|
||||
_add_arguments(param, parser, used_char_args, add_nos)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
_DOCSTRING_SPLIT = compile_regex(r'\n\s*-{4,}\s*\n')
|
||||
|
||||
|
||||
def parse_docstring(docstring):
|
||||
'''
|
||||
Given a docstring, parse it into a description and epilog part
|
||||
'''
|
||||
if docstring is None:
|
||||
return '', ''
|
||||
|
||||
parts = _DOCSTRING_SPLIT.split(docstring)
|
||||
|
||||
if len(parts) == 1:
|
||||
return docstring, ''
|
||||
elif len(parts) == 2:
|
||||
return parts[0], parts[1]
|
||||
else:
|
||||
raise TooManySplitsError()
|
||||
|
||||
|
||||
def autoparse(
|
||||
func=None, *,
|
||||
description=None,
|
||||
epilog=None,
|
||||
add_nos=False,
|
||||
parser=None):
|
||||
'''
|
||||
This decorator converts a function that takes normal arguments into a
|
||||
function which takes a single optional argument, argv, parses it using an
|
||||
argparse.ArgumentParser, and calls the underlying function with the parsed
|
||||
arguments. If it is not given, sys.argv[1:] is used. This is so that the
|
||||
function can be used as a setuptools entry point, as well as a normal main
|
||||
function. sys.argv[1:] is not evaluated until the function is called, to
|
||||
allow injecting different arguments for testing.
|
||||
|
||||
It uses the argument signature of the function to create an
|
||||
ArgumentParser. Parameters without defaults become positional parameters,
|
||||
while parameters *with* defaults become --options. Use annotations to set
|
||||
the type of the parameter.
|
||||
|
||||
The `desctiption` and `epilog` parameters corrospond to the same respective
|
||||
argparse parameters. If no description is given, it defaults to the
|
||||
decorated functions's docstring, if present.
|
||||
|
||||
If add_nos is True, every boolean option (that is, every parameter with a
|
||||
default of True/False or a type of bool) will have a --no- version created
|
||||
as well, which inverts the option. For instance, the --verbose option will
|
||||
have a --no-verbose counterpart. These are not mutually exclusive-
|
||||
whichever one appears last in the argument list will have precedence.
|
||||
|
||||
If a parser is given, it is used instead of one generated from the function
|
||||
signature. In this case, no parser is created; instead, the given parser is
|
||||
used to parse the argv argument. The parser's results' argument names must
|
||||
match up with the parameter names of the decorated function.
|
||||
|
||||
The decorated function is attached to the result as the `func` attribute,
|
||||
and the parser is attached as the `parser` attribute.
|
||||
'''
|
||||
|
||||
# If @autoparse(...) is used instead of @autoparse
|
||||
if func is None:
|
||||
return lambda f: autoparse(
|
||||
f, description=description,
|
||||
epilog=epilog,
|
||||
add_nos=add_nos,
|
||||
parser=parser)
|
||||
|
||||
func_sig = signature(func)
|
||||
|
||||
docstr_description, docstr_epilog = parse_docstring(getdoc(func))
|
||||
|
||||
if parser is None:
|
||||
parser = make_parser(
|
||||
func_sig,
|
||||
description or docstr_description,
|
||||
epilog or docstr_epilog,
|
||||
add_nos)
|
||||
|
||||
@wraps(func)
|
||||
def autoparse_wrapper(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
# Get empty argument binding, to fill with parsed arguments. This
|
||||
# object does all the heavy lifting of turning named arguments into
|
||||
# into correctly bound *args and **kwargs.
|
||||
parsed_args = func_sig.bind_partial()
|
||||
parsed_args.arguments.update(vars(parser.parse_args(argv)))
|
||||
|
||||
return func(*parsed_args.args, **parsed_args.kwargs)
|
||||
|
||||
# TODO: attach an updated __signature__ to autoparse_wrapper, just in case.
|
||||
|
||||
# Attach the wrapped function and parser, and return the wrapper.
|
||||
autoparse_wrapper.func = func
|
||||
autoparse_wrapper.parser = parser
|
||||
return autoparse_wrapper
|
||||
|
||||
|
||||
@contextmanager
|
||||
def smart_open(filename_or_file, *args, **kwargs):
|
||||
'''
|
||||
This context manager allows you to open a filename, if you want to default
|
||||
some already-existing file object, like sys.stdout, which shouldn't be
|
||||
closed at the end of the context. If the filename argument is a str, bytes,
|
||||
or int, the file object is created via a call to open with the given *args
|
||||
and **kwargs, sent to the context, and closed at the end of the context,
|
||||
just like "with open(filename) as f:". If it isn't one of the openable
|
||||
types, the object simply sent to the context unchanged, and left unclosed
|
||||
at the end of the context. Example:
|
||||
|
||||
def work_with_file(name=sys.stdout):
|
||||
with smart_open(name) as f:
|
||||
# Works correctly if name is a str filename or sys.stdout
|
||||
print("Some stuff", file=f)
|
||||
# If it was a filename, f is closed at the end here.
|
||||
'''
|
||||
if isinstance(filename_or_file, (str, bytes, int)):
|
||||
with open(filename_or_file, *args, **kwargs) as file:
|
||||
yield file
|
||||
else:
|
||||
yield filename_or_file
|
||||
@ -0,0 +1,23 @@
|
||||
# Copyright 2014-2016 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class AutocommandError(Exception):
|
||||
'''Base class for autocommand exceptions'''
|
||||
pass
|
||||
|
||||
# Individual modules will define errors specific to that module.
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,30 @@
|
||||
Copyright © 2004-2020, CherryPy Team (team@cherrypy.dev)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
* * *
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* 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.
|
||||
|
||||
* Neither the name of CherryPy 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,150 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: cheroot
|
||||
Version: 10.0.0
|
||||
Summary: Highly-optimized, pure-python HTTP server
|
||||
Home-page: https://cheroot.cherrypy.dev
|
||||
Author: CherryPy Team
|
||||
Author-email: team@cherrypy.dev
|
||||
License: UNKNOWN
|
||||
Project-URL: CI: GitHub, https://github.com/cherrypy/cheroot/actions
|
||||
Project-URL: Docs: RTD, https://cheroot.cherrypy.dev
|
||||
Project-URL: GitHub: issues, https://github.com/cherrypy/cheroot/issues
|
||||
Project-URL: GitHub: repo, https://github.com/cherrypy/cheroot
|
||||
Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=referral&utm_campaign=pypi
|
||||
Keywords: http,server,ssl,wsgi
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Framework :: CherryPy
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: Implementation
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: Jython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Internet :: WWW/HTTP
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.6
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.md
|
||||
Requires-Dist: more-itertools (>=2.6)
|
||||
Requires-Dist: jaraco.functools
|
||||
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx (>=1.8.2) ; extra == 'docs'
|
||||
Requires-Dist: jaraco.packaging (>=3.2) ; extra == 'docs'
|
||||
Requires-Dist: sphinx-tabs (>=1.1.0) ; extra == 'docs'
|
||||
Requires-Dist: furo ; extra == 'docs'
|
||||
Requires-Dist: python-dateutil ; extra == 'docs'
|
||||
Requires-Dist: sphinxcontrib-apidoc (>=0.3.0) ; extra == 'docs'
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg
|
||||
:target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md
|
||||
:alt: SWUbanner
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/cheroot.svg
|
||||
:target: https://pypi.org/project/cheroot
|
||||
|
||||
.. image:: https://tidelift.com/badges/package/pypi/cheroot
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=readme
|
||||
:alt: Cheroot is available as part of the Tidelift Subscription
|
||||
|
||||
.. image:: https://github.com/cherrypy/cheroot/actions/workflows/ci-cd.yml/badge.svg
|
||||
:target: https://github.com/cherrypy/cheroot/actions/workflows/ci-cd.yml
|
||||
:alt: GitHub Actions CI/CD Workflow
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600
|
||||
:target: https://pypi.org/project/cheroot
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/cheroot.svg
|
||||
:target: https://pypi.org/project/cheroot
|
||||
|
||||
.. image:: https://codecov.io/gh/cherrypy/cheroot/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/cherrypy/cheroot
|
||||
:alt: codecov
|
||||
|
||||
.. image:: https://readthedocs.org/projects/cheroot/badge/?version=latest
|
||||
:target: https://cheroot.cherrypy.dev/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://img.shields.io/badge/StackOverflow-Cheroot-blue.svg
|
||||
:target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy
|
||||
|
||||
.. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg
|
||||
:target: https://gitter.im/cherrypy/cherrypy
|
||||
|
||||
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
||||
:target: http://makeapullrequest.com/
|
||||
|
||||
.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot.svg?type=shield
|
||||
:target: https://app.fossa.io/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot?ref=badge_shield
|
||||
:alt: FOSSA Status
|
||||
|
||||
Cheroot is the high-performance, pure-Python HTTP server used by CherryPy.
|
||||
|
||||
Status
|
||||
======
|
||||
|
||||
The test suite currently relies on pytest. It's being run via GitHub
|
||||
Actions CI/CD workflows.
|
||||
|
||||
For Enterprise
|
||||
==============
|
||||
|
||||
.. list-table::
|
||||
:widths: 10 100
|
||||
|
||||
* - |tideliftlogo|
|
||||
- Professional support for Cheroot is available as part of the
|
||||
`Tidelift Subscription`_. The CherryPy maintainers and the
|
||||
maintainers of thousands of other packages are working with
|
||||
Tidelift to deliver one enterprise subscription that covers all
|
||||
of the open source you use.
|
||||
|
||||
Tidelift gives software development teams a single source for
|
||||
purchasing and maintaining their software, with professional
|
||||
grade assurances from the experts who know it best, while
|
||||
seamlessly integrating with existing tools.
|
||||
|
||||
`Learn more <Tidelift Subscription_>`_.
|
||||
|
||||
.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=referral&utm_campaign=readme
|
||||
|
||||
.. |tideliftlogo| image:: https://cdn2.hubspot.net/hubfs/4008838/website/logos/logos_for_download/Tidelift_primary-shorthand-logo.png
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=readme
|
||||
:width: 75
|
||||
:alt: Tidelift
|
||||
|
||||
Contribute Cheroot
|
||||
==================
|
||||
**Want to add something to upstream?** Feel free to submit a PR or file an issue
|
||||
if unsure. Please follow `CherryPy's common contribution guidelines
|
||||
<https://github.com/cherrypy/cherrypy/blob/master/.github/CONTRIBUTING.rst>`_.
|
||||
Note that PR is more likely to be accepted if it includes tests and detailed
|
||||
description helping maintainers to understand it better 🎉
|
||||
|
||||
Oh, and be pythonic, please 🐍
|
||||
|
||||
**Don't know how?** Check out `How to Contribute to Open Source
|
||||
<https://opensource.guide/how-to-contribute/>`_ article by GitHub 🚀
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot.svg?type=large
|
||||
:target: https://app.fossa.io/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot?ref=badge_large
|
||||
:alt: FOSSA Status
|
||||
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
../../../bin/cheroot,sha256=E8JQfKtdh5qRqSjMrCA02q8ZRqP-O-sRTc4SsgO4-3s,243
|
||||
cheroot-10.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
cheroot-10.0.0.dist-info/LICENSE.md,sha256=4g_utJGn6YCE8VcZNJ6YV6rUHEUDxeR5-IFbBj2_dWQ,1511
|
||||
cheroot-10.0.0.dist-info/METADATA,sha256=NEVNG_PxvbSNFehtACADPaqBN5jArY3OBntPlbPrAcE,6423
|
||||
cheroot-10.0.0.dist-info/RECORD,,
|
||||
cheroot-10.0.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||
cheroot-10.0.0.dist-info/entry_points.txt,sha256=BapQtPZPUE38-YhhtJUBwlkNRNY_yg0s3_bt5yVqqG8,46
|
||||
cheroot-10.0.0.dist-info/top_level.txt,sha256=P8VZfrem5gTRS34X6Thu7jyEoj_zSaPNI_3P0fShbAI,8
|
||||
cheroot/__init__.py,sha256=ZZgcItIBeyj6d3xlBw_o4uFxtBwL86Y9MwzFSfmd1_c,284
|
||||
cheroot/__init__.pyi,sha256=Y25n44pyE3vp92MiABKrcK3IWRyQ1JG1rZ4Ufqy2nC0,17
|
||||
cheroot/__main__.py,sha256=jLOqwD221LYbg1ksPWhjOKYu4M4yV7y3mteM6CJi-Oc,109
|
||||
cheroot/__pycache__/__init__.cpython-310.pyc,,
|
||||
cheroot/__pycache__/__main__.cpython-310.pyc,,
|
||||
cheroot/__pycache__/_compat.cpython-310.pyc,,
|
||||
cheroot/__pycache__/cli.cpython-310.pyc,,
|
||||
cheroot/__pycache__/connections.cpython-310.pyc,,
|
||||
cheroot/__pycache__/errors.cpython-310.pyc,,
|
||||
cheroot/__pycache__/makefile.cpython-310.pyc,,
|
||||
cheroot/__pycache__/server.cpython-310.pyc,,
|
||||
cheroot/__pycache__/testing.cpython-310.pyc,,
|
||||
cheroot/__pycache__/wsgi.cpython-310.pyc,,
|
||||
cheroot/_compat.py,sha256=t1y0uqANmAv4SfkFrqHUk6NCgWOXkt2W8_1r0u7EQTA,2083
|
||||
cheroot/_compat.pyi,sha256=jrBkeGVNS6B6TXzP0NyyBQyGvf-ucCFbLi5RitP1eJs,605
|
||||
cheroot/cli.py,sha256=5qxJ0tK7nUPwTI2Q97VyjlPNdVwznoj-gTT7DJ_y_S8,6987
|
||||
cheroot/cli.pyi,sha256=LIKNaRFyZVRRl2n3Jm_VTJjYaNsDSJQ7IcEr5qcjZR0,828
|
||||
cheroot/connections.py,sha256=a04R6pFRn9uvtgO_gDXvJwEy0Asqrxuy4ks5rkG7gY4,14395
|
||||
cheroot/connections.pyi,sha256=r-I9Mkn-PHcjqQvekmMmQNbh8a5-sJyfLp6d5sP7T48,714
|
||||
cheroot/errors.py,sha256=vOHGdmaJwk9eUu-XqTyYjYxCdC6DgdTJHSm0bE3soIc,2753
|
||||
cheroot/errors.pyi,sha256=WPJaht4vEcELxTStNPMYuEmztK52aLbOcYE8MwGTmSU,425
|
||||
cheroot/makefile.py,sha256=XVLIM1ngS_XbLzZryUPgs5SSnV8CHyyRGzU3J4QpVPE,2306
|
||||
cheroot/makefile.pyi,sha256=39bXRM-9N1jOAEgfudimuqoOkLFAShg-naldaQDKnGM,542
|
||||
cheroot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cheroot/server.py,sha256=0_mLoRq7evFYoJVxKV0ia6wU2D4DuVGj49oVDv6FPHs,77336
|
||||
cheroot/server.pyi,sha256=_r4KsLNMGvh6UZHN-hYrz7VAhut4CfrTHmNoyWauYoE,5064
|
||||
cheroot/ssl/__init__.py,sha256=U2aplZGnvXibKBSGN3Y9cje_zFS-mA_bJz5jO9h7JaY,1416
|
||||
cheroot/ssl/__init__.pyi,sha256=liQgp4uYOywZfAGCqcaxVYEM6OBFCWOS-bCGtDI2q2w,581
|
||||
cheroot/ssl/__pycache__/__init__.cpython-310.pyc,,
|
||||
cheroot/ssl/__pycache__/builtin.cpython-310.pyc,,
|
||||
cheroot/ssl/__pycache__/pyopenssl.cpython-310.pyc,,
|
||||
cheroot/ssl/builtin.py,sha256=2apU5DsSFCTVysj53Ek-zKb3YH5V01j5MCWwTd3GK7M,17906
|
||||
cheroot/ssl/builtin.pyi,sha256=nriJS-4eEkyw04oOZW8dslIXLfr_Kzkb7izy6TavEIw,561
|
||||
cheroot/ssl/pyopenssl.py,sha256=3aVW3aQYW6YOHoQyPYaJ46Ts1EQ5_yZCM5g9SnvFQzs,13231
|
||||
cheroot/ssl/pyopenssl.pyi,sha256=YlpXT8Rpj_mvBtOBiFuBep758p618JSegch9x2OCP38,1086
|
||||
cheroot/test/__init__.py,sha256=_hgyWgeLHsLLScMhGwMOsevD3Wg3pssK535YfNJFoeU,26
|
||||
cheroot/test/__pycache__/__init__.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/_pytest_plugin.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/conftest.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/helper.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test__compat.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_cli.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_conn.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_core.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_dispatch.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_errors.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_makefile.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_server.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_ssl.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/test_wsgi.cpython-310.pyc,,
|
||||
cheroot/test/__pycache__/webtest.cpython-310.pyc,,
|
||||
cheroot/test/_pytest_plugin.py,sha256=u_13sGqGbIDP7z9ZC1_SRr_DWBoDQDR3O37IPqOxwyI,1768
|
||||
cheroot/test/conftest.py,sha256=pyMICEAvtvWw1Q-sqhr-VVcal3NbWlDD3rKe9_ROsGM,2051
|
||||
cheroot/test/helper.py,sha256=A5HlkxxyswxYRT_0IjHja_TWvd2VxDY44Elzni0uke0,4755
|
||||
cheroot/test/test__compat.py,sha256=uC4QuuYSDDMyErGij-vHfGV1fKTA_ylnZEmWXMLXUvc,1661
|
||||
cheroot/test/test_cli.py,sha256=7bmQNozJ8cakCFw-ogUzAZq7yGUaSJYwzgXQx8tQfLg,2664
|
||||
cheroot/test/test_conn.py,sha256=kpk30-tHHRIgeLWEaAC7iF39FoOv42WnPsdmCZkdTkY,43780
|
||||
cheroot/test/test_core.py,sha256=6MOYMEvXZhx4wnwOt8uTO-1Cu-37n5qqLiB0hicCeD8,14698
|
||||
cheroot/test/test_dispatch.py,sha256=NJtth_qaHlKa7V83j-ekrf_8G2-7HI8EQiZdKTUcCx4,1210
|
||||
cheroot/test/test_errors.py,sha256=i4-o3CTwluDN_70N9c_9QI4OY1imIMHSqOHpmyqML_8,924
|
||||
cheroot/test/test_makefile.py,sha256=j3ZVMMDJI_JPTun-agdaJSKa8z3kH5HhXv_95Lg9T2I,1191
|
||||
cheroot/test/test_server.py,sha256=vBZ35OJOGEgBcPmjpomwMIuCBoIlGSKc0OHS1EnhQqY,16697
|
||||
cheroot/test/test_ssl.py,sha256=CkbF164i-yV5Infp8zE7sXSRZQOxlIILulu3eWRhM90,22268
|
||||
cheroot/test/test_wsgi.py,sha256=vdyH1JN2K8qbfx4olnda-VHOxErtemaVVEUjVlNC5mo,2823
|
||||
cheroot/test/webtest.py,sha256=3tjaCGiotSoEX9s1ULVKQojqLm82z3CDKZSrZQLzSTI,18398
|
||||
cheroot/testing.py,sha256=ilu4oOPR8Qap64mO5G6PyLjXD0ix6yJiRY5K0Ldqi6s,3969
|
||||
cheroot/testing.pyi,sha256=HlbbwoROyn5DSAuCQxcUw58v2cCKu1MG7ySzwbH0ZXk,448
|
||||
cheroot/workers/__init__.py,sha256=-ziyw7iPWHs2dN4R_Q7AQZ7r0dQPTes1nVCzAg2LOc8,25
|
||||
cheroot/workers/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cheroot/workers/__pycache__/__init__.cpython-310.pyc,,
|
||||
cheroot/workers/__pycache__/threadpool.cpython-310.pyc,,
|
||||
cheroot/workers/threadpool.py,sha256=F6KusLUcVQmooIV35hXhDc3ckaTrZkonJORU74p6vcE,10967
|
||||
cheroot/workers/threadpool.pyi,sha256=9xF6s4LAwnURJbdG1K8f97zGk2YVb2kEULJMaxsUXiI,925
|
||||
cheroot/wsgi.py,sha256=_eUOnbMUvc6wdzaSjEyQrUuu6Q6ye5yYNC7z9rovnCw,14239
|
||||
cheroot/wsgi.pyi,sha256=4DVIwCgcYtIxIA7wd-ZHcBNKOSiaXwaAAwrsUcg0h0M,1516
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.37.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
[console_scripts]
|
||||
cheroot = cheroot.cli:main
|
||||
|
||||
@ -0,0 +1 @@
|
||||
cheroot
|
||||
@ -0,0 +1,12 @@
|
||||
"""High-performance, pure-Python HTTP server used by CherryPy."""
|
||||
|
||||
try:
|
||||
from importlib import metadata
|
||||
except ImportError:
|
||||
import importlib_metadata as metadata # noqa: WPS440
|
||||
|
||||
|
||||
try:
|
||||
__version__ = metadata.version('cheroot')
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
@ -0,0 +1 @@
|
||||
__version__: str
|
||||
@ -0,0 +1,6 @@
|
||||
"""Stub for accessing the Cheroot CLI tool."""
|
||||
|
||||
from .cli import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
84
awesome_venv/lib/python3.10/site-packages/cheroot/_compat.py
Normal file
84
awesome_venv/lib/python3.10/site-packages/cheroot/_compat.py
Normal file
@ -0,0 +1,84 @@
|
||||
# pylint: disable=unused-import
|
||||
"""Compatibility code for using Cheroot with various versions of Python."""
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
IS_ABOVE_OPENSSL10 = ssl.OPENSSL_VERSION_INFO >= (1, 1)
|
||||
del ssl
|
||||
except ImportError:
|
||||
IS_ABOVE_OPENSSL10 = None
|
||||
|
||||
|
||||
IS_CI = bool(os.getenv('CI'))
|
||||
IS_GITHUB_ACTIONS_WORKFLOW = bool(os.getenv('GITHUB_WORKFLOW'))
|
||||
|
||||
|
||||
IS_PYPY = platform.python_implementation() == 'PyPy'
|
||||
|
||||
|
||||
SYS_PLATFORM = platform.system()
|
||||
IS_WINDOWS = SYS_PLATFORM == 'Windows'
|
||||
IS_LINUX = SYS_PLATFORM == 'Linux'
|
||||
IS_MACOS = SYS_PLATFORM == 'Darwin'
|
||||
IS_SOLARIS = SYS_PLATFORM == 'SunOS'
|
||||
|
||||
PLATFORM_ARCH = platform.machine()
|
||||
IS_PPC = PLATFORM_ARCH.startswith('ppc')
|
||||
|
||||
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the native string as bytes in the given encoding."""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n.encode(encoding)
|
||||
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the native string as Unicode with the given encoding."""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n
|
||||
|
||||
|
||||
def bton(b, encoding='ISO-8859-1'):
|
||||
"""Return the byte string as native string in the given encoding."""
|
||||
return b.decode(encoding)
|
||||
|
||||
|
||||
def assert_native(n):
|
||||
"""Check whether the input is of native :py:class:`str` type.
|
||||
|
||||
Raises:
|
||||
TypeError: in case of failed check
|
||||
|
||||
"""
|
||||
if not isinstance(n, str):
|
||||
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
|
||||
|
||||
|
||||
def extract_bytes(mv):
|
||||
r"""Retrieve bytes out of the given input buffer.
|
||||
|
||||
:param mv: input :py:func:`buffer`
|
||||
:type mv: memoryview or bytes
|
||||
|
||||
:return: unwrapped bytes
|
||||
:rtype: bytes
|
||||
|
||||
:raises ValueError: if the input is not one of \
|
||||
:py:class:`memoryview`/:py:func:`buffer` \
|
||||
or :py:class:`bytes`
|
||||
"""
|
||||
if isinstance(mv, memoryview):
|
||||
return mv.tobytes()
|
||||
|
||||
if isinstance(mv, bytes):
|
||||
return mv
|
||||
|
||||
raise ValueError(
|
||||
'extract_bytes() only accepts bytes and memoryview/buffer',
|
||||
)
|
||||
@ -0,0 +1,22 @@
|
||||
from typing import Any, ContextManager, Optional, Type, Union
|
||||
|
||||
def suppress(*exceptions: Type[BaseException]) -> ContextManager[None]: ...
|
||||
|
||||
IS_ABOVE_OPENSSL10: Optional[bool]
|
||||
IS_CI: bool
|
||||
IS_GITHUB_ACTIONS_WORKFLOW: bool
|
||||
IS_PYPY: bool
|
||||
SYS_PLATFORM: str
|
||||
IS_WINDOWS: bool
|
||||
IS_LINUX: bool
|
||||
IS_MACOS: bool
|
||||
IS_SOLARIS: bool
|
||||
PLATFORM_ARCH: str
|
||||
IS_PPC: bool
|
||||
|
||||
def ntob(n: str, encoding: str = ...) -> bytes: ...
|
||||
def ntou(n: str, encoding: str = ...) -> str: ...
|
||||
def bton(b: bytes, encoding: str = ...) -> str: ...
|
||||
def assert_native(n: str) -> None: ...
|
||||
|
||||
def extract_bytes(mv: Union[memoryview, bytes]) -> bytes: ...
|
||||
243
awesome_venv/lib/python3.10/site-packages/cheroot/cli.py
Normal file
243
awesome_venv/lib/python3.10/site-packages/cheroot/cli.py
Normal file
@ -0,0 +1,243 @@
|
||||
"""Command line tool for starting a Cheroot WSGI/HTTP server instance.
|
||||
|
||||
Basic usage:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ # Start a server on 127.0.0.1:8000 with the default settings
|
||||
$ # for the WSGI app myapp/wsgi.py:application()
|
||||
$ cheroot myapp.wsgi
|
||||
|
||||
$ # Start a server on 0.0.0.0:9000 with 8 threads
|
||||
$ # for the WSGI app myapp/wsgi.py:main_app()
|
||||
$ cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8
|
||||
|
||||
$ # Start a server for the cheroot.server.Gateway subclass
|
||||
$ # myapp/gateway.py:HTTPGateway
|
||||
$ cheroot myapp.gateway:HTTPGateway
|
||||
|
||||
$ # Start a server on the UNIX socket /var/spool/myapp.sock
|
||||
$ cheroot myapp.wsgi --bind /var/spool/myapp.sock
|
||||
|
||||
$ # Start a server on the abstract UNIX socket CherootServer
|
||||
$ cheroot myapp.wsgi --bind @CherootServer
|
||||
|
||||
.. spelling::
|
||||
|
||||
cli
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse # noqa: WPS301
|
||||
from importlib import import_module
|
||||
from contextlib import suppress
|
||||
|
||||
from . import server
|
||||
from . import wsgi
|
||||
|
||||
|
||||
class BindLocation:
|
||||
"""A class for storing the bind location for a Cheroot instance."""
|
||||
|
||||
|
||||
class TCPSocket(BindLocation):
|
||||
"""TCPSocket."""
|
||||
|
||||
def __init__(self, address, port):
|
||||
"""Initialize.
|
||||
|
||||
Args:
|
||||
address (str): Host name or IP address
|
||||
port (int): TCP port number
|
||||
|
||||
"""
|
||||
self.bind_addr = address, port
|
||||
|
||||
|
||||
class UnixSocket(BindLocation):
|
||||
"""UnixSocket."""
|
||||
|
||||
def __init__(self, path):
|
||||
"""Initialize."""
|
||||
self.bind_addr = path
|
||||
|
||||
|
||||
class AbstractSocket(BindLocation):
|
||||
"""AbstractSocket."""
|
||||
|
||||
def __init__(self, abstract_socket):
|
||||
"""Initialize."""
|
||||
self.bind_addr = '\x00{sock_path}'.format(sock_path=abstract_socket)
|
||||
|
||||
|
||||
class Application:
|
||||
"""Application."""
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, full_path):
|
||||
"""Read WSGI app/Gateway path string and import application module."""
|
||||
mod_path, _, app_path = full_path.partition(':')
|
||||
app = getattr(import_module(mod_path), app_path or 'application')
|
||||
# suppress the `TypeError` exception, just in case `app` is not a class
|
||||
with suppress(TypeError):
|
||||
if issubclass(app, server.Gateway):
|
||||
return GatewayYo(app)
|
||||
|
||||
return cls(app)
|
||||
|
||||
def __init__(self, wsgi_app):
|
||||
"""Initialize."""
|
||||
if not callable(wsgi_app):
|
||||
raise TypeError(
|
||||
'Application must be a callable object or '
|
||||
'cheroot.server.Gateway subclass',
|
||||
)
|
||||
self.wsgi_app = wsgi_app
|
||||
|
||||
def server_args(self, parsed_args):
|
||||
"""Return keyword args for Server class."""
|
||||
args = {
|
||||
arg: value
|
||||
for arg, value in vars(parsed_args).items()
|
||||
if not arg.startswith('_') and value is not None
|
||||
}
|
||||
args.update(vars(self))
|
||||
return args
|
||||
|
||||
def server(self, parsed_args):
|
||||
"""Server."""
|
||||
return wsgi.Server(**self.server_args(parsed_args))
|
||||
|
||||
|
||||
class GatewayYo:
|
||||
"""Gateway."""
|
||||
|
||||
def __init__(self, gateway):
|
||||
"""Init."""
|
||||
self.gateway = gateway
|
||||
|
||||
def server(self, parsed_args):
|
||||
"""Server."""
|
||||
server_args = vars(self)
|
||||
server_args['bind_addr'] = parsed_args['bind_addr']
|
||||
if parsed_args.max is not None:
|
||||
server_args['maxthreads'] = parsed_args.max
|
||||
if parsed_args.numthreads is not None:
|
||||
server_args['minthreads'] = parsed_args.numthreads
|
||||
return server.HTTPServer(**server_args)
|
||||
|
||||
|
||||
def parse_wsgi_bind_location(bind_addr_string):
|
||||
"""Convert bind address string to a BindLocation."""
|
||||
# if the string begins with an @ symbol, use an abstract socket,
|
||||
# this is the first condition to verify, otherwise the urlparse
|
||||
# validation would detect //@<value> as a valid url with a hostname
|
||||
# with value: "<value>" and port: None
|
||||
if bind_addr_string.startswith('@'):
|
||||
return AbstractSocket(bind_addr_string[1:])
|
||||
|
||||
# try and match for an IP/hostname and port
|
||||
match = urllib.parse.urlparse(
|
||||
'//{addr}'.format(addr=bind_addr_string),
|
||||
)
|
||||
try:
|
||||
addr = match.hostname
|
||||
port = match.port
|
||||
if addr is not None or port is not None:
|
||||
return TCPSocket(addr, port)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# else, assume a UNIX socket path
|
||||
return UnixSocket(path=bind_addr_string)
|
||||
|
||||
|
||||
def parse_wsgi_bind_addr(bind_addr_string):
|
||||
"""Convert bind address string to bind address parameter."""
|
||||
return parse_wsgi_bind_location(bind_addr_string).bind_addr
|
||||
|
||||
|
||||
_arg_spec = {
|
||||
'_wsgi_app': {
|
||||
'metavar': 'APP_MODULE',
|
||||
'type': Application.resolve,
|
||||
'help': 'WSGI application callable or cheroot.server.Gateway subclass',
|
||||
},
|
||||
'--bind': {
|
||||
'metavar': 'ADDRESS',
|
||||
'dest': 'bind_addr',
|
||||
'type': parse_wsgi_bind_addr,
|
||||
'default': '[::1]:8000',
|
||||
'help': 'Network interface to listen on (default: [::1]:8000)',
|
||||
},
|
||||
'--chdir': {
|
||||
'metavar': 'PATH',
|
||||
'type': os.chdir,
|
||||
'help': 'Set the working directory',
|
||||
},
|
||||
'--server-name': {
|
||||
'dest': 'server_name',
|
||||
'type': str,
|
||||
'help': 'Web server name to be advertised via Server HTTP header',
|
||||
},
|
||||
'--threads': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'numthreads',
|
||||
'type': int,
|
||||
'help': 'Minimum number of worker threads',
|
||||
},
|
||||
'--max-threads': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'max',
|
||||
'type': int,
|
||||
'help': 'Maximum number of worker threads',
|
||||
},
|
||||
'--timeout': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'timeout',
|
||||
'type': int,
|
||||
'help': 'Timeout in seconds for accepted connections',
|
||||
},
|
||||
'--shutdown-timeout': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'shutdown_timeout',
|
||||
'type': int,
|
||||
'help': 'Time in seconds to wait for worker threads to cleanly exit',
|
||||
},
|
||||
'--request-queue-size': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'request_queue_size',
|
||||
'type': int,
|
||||
'help': 'Maximum number of queued connections',
|
||||
},
|
||||
'--accepted-queue-size': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'accepted_queue_size',
|
||||
'type': int,
|
||||
'help': 'Maximum number of active requests in queue',
|
||||
},
|
||||
'--accepted-queue-timeout': {
|
||||
'metavar': 'INT',
|
||||
'dest': 'accepted_queue_timeout',
|
||||
'type': int,
|
||||
'help': 'Timeout in seconds for putting requests into queue',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Create a new Cheroot instance with arguments from the command line."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Start an instance of the Cheroot WSGI/HTTP server.',
|
||||
)
|
||||
for arg, spec in _arg_spec.items():
|
||||
parser.add_argument(arg, **spec)
|
||||
raw_args = parser.parse_args()
|
||||
|
||||
# ensure cwd in sys.path
|
||||
'' in sys.path or sys.path.insert(0, '')
|
||||
|
||||
# create a server based on the arguments provided
|
||||
raw_args._wsgi_app.server(raw_args).safe_start()
|
||||
32
awesome_venv/lib/python3.10/site-packages/cheroot/cli.pyi
Normal file
32
awesome_venv/lib/python3.10/site-packages/cheroot/cli.pyi
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Any
|
||||
|
||||
class BindLocation: ...
|
||||
|
||||
class TCPSocket(BindLocation):
|
||||
bind_addr: Any
|
||||
def __init__(self, address, port) -> None: ...
|
||||
|
||||
class UnixSocket(BindLocation):
|
||||
bind_addr: Any
|
||||
def __init__(self, path) -> None: ...
|
||||
|
||||
class AbstractSocket(BindLocation):
|
||||
bind_addr: Any
|
||||
def __init__(self, abstract_socket) -> None: ...
|
||||
|
||||
class Application:
|
||||
@classmethod
|
||||
def resolve(cls, full_path): ...
|
||||
wsgi_app: Any
|
||||
def __init__(self, wsgi_app) -> None: ...
|
||||
def server_args(self, parsed_args): ...
|
||||
def server(self, parsed_args): ...
|
||||
|
||||
class GatewayYo:
|
||||
gateway: Any
|
||||
def __init__(self, gateway) -> None: ...
|
||||
def server(self, parsed_args): ...
|
||||
|
||||
def parse_wsgi_bind_location(bind_addr_string: str): ...
|
||||
def parse_wsgi_bind_addr(bind_addr_string: str): ...
|
||||
def main() -> None: ...
|
||||
387
awesome_venv/lib/python3.10/site-packages/cheroot/connections.py
Normal file
387
awesome_venv/lib/python3.10/site-packages/cheroot/connections.py
Normal file
@ -0,0 +1,387 @@
|
||||
"""Utilities to manage open connections."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import selectors
|
||||
from contextlib import suppress
|
||||
|
||||
from . import errors
|
||||
from ._compat import IS_WINDOWS
|
||||
from .makefile import MakeFile
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
try:
|
||||
from ctypes import windll, WinError
|
||||
import ctypes.wintypes
|
||||
_SetHandleInformation = windll.kernel32.SetHandleInformation
|
||||
_SetHandleInformation.argtypes = [
|
||||
ctypes.wintypes.HANDLE,
|
||||
ctypes.wintypes.DWORD,
|
||||
ctypes.wintypes.DWORD,
|
||||
]
|
||||
_SetHandleInformation.restype = ctypes.wintypes.BOOL
|
||||
except ImportError:
|
||||
def prevent_socket_inheritance(sock):
|
||||
"""Stub inheritance prevention.
|
||||
|
||||
Dummy function, since neither fcntl nor ctypes are available.
|
||||
"""
|
||||
pass
|
||||
else:
|
||||
def prevent_socket_inheritance(sock):
|
||||
"""Mark the given socket fd as non-inheritable (Windows)."""
|
||||
if not _SetHandleInformation(sock.fileno(), 1, 0):
|
||||
raise WinError()
|
||||
else:
|
||||
def prevent_socket_inheritance(sock):
|
||||
"""Mark the given socket fd as non-inheritable (POSIX)."""
|
||||
fd = sock.fileno()
|
||||
old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
|
||||
|
||||
|
||||
class _ThreadsafeSelector:
|
||||
"""Thread-safe wrapper around a DefaultSelector.
|
||||
|
||||
There are 2 thread contexts in which it may be accessed:
|
||||
* the selector thread
|
||||
* one of the worker threads in workers/threadpool.py
|
||||
|
||||
The expected read/write patterns are:
|
||||
* :py:func:`~iter`: selector thread
|
||||
* :py:meth:`register`: selector thread and threadpool,
|
||||
via :py:meth:`~cheroot.workers.threadpool.ThreadPool.put`
|
||||
* :py:meth:`unregister`: selector thread only
|
||||
|
||||
Notably, this means :py:class:`_ThreadsafeSelector` never needs to worry
|
||||
that connections will be removed behind its back.
|
||||
|
||||
The lock is held when iterating or modifying the selector but is not
|
||||
required when :py:meth:`select()ing <selectors.BaseSelector.select>` on it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._selector = selectors.DefaultSelector()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def __len__(self):
|
||||
with self._lock:
|
||||
return len(self._selector.get_map() or {})
|
||||
|
||||
@property
|
||||
def connections(self):
|
||||
"""Retrieve connections registered with the selector."""
|
||||
with self._lock:
|
||||
mapping = self._selector.get_map() or {}
|
||||
for _, (_, sock_fd, _, conn) in mapping.items():
|
||||
yield (sock_fd, conn)
|
||||
|
||||
def register(self, fileobj, events, data=None):
|
||||
"""Register ``fileobj`` with the selector."""
|
||||
with self._lock:
|
||||
return self._selector.register(fileobj, events, data)
|
||||
|
||||
def unregister(self, fileobj):
|
||||
"""Unregister ``fileobj`` from the selector."""
|
||||
with self._lock:
|
||||
return self._selector.unregister(fileobj)
|
||||
|
||||
def select(self, timeout=None):
|
||||
"""Return socket fd and data pairs from selectors.select call.
|
||||
|
||||
Returns entries ready to read in the form:
|
||||
(socket_file_descriptor, connection)
|
||||
"""
|
||||
return (
|
||||
(key.fd, key.data)
|
||||
for key, _ in self._selector.select(timeout=timeout)
|
||||
)
|
||||
|
||||
def close(self):
|
||||
"""Close the selector."""
|
||||
with self._lock:
|
||||
self._selector.close()
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Class which manages HTTPConnection objects.
|
||||
|
||||
This is for connections which are being kept-alive for follow-up requests.
|
||||
"""
|
||||
|
||||
def __init__(self, server):
|
||||
"""Initialize ConnectionManager object.
|
||||
|
||||
Args:
|
||||
server (cheroot.server.HTTPServer): web server object
|
||||
that uses this ConnectionManager instance.
|
||||
"""
|
||||
self._serving = False
|
||||
self._stop_requested = False
|
||||
|
||||
self.server = server
|
||||
self._selector = _ThreadsafeSelector()
|
||||
|
||||
self._selector.register(
|
||||
server.socket.fileno(),
|
||||
selectors.EVENT_READ, data=server,
|
||||
)
|
||||
|
||||
def put(self, conn):
|
||||
"""Put idle connection into the ConnectionManager to be managed.
|
||||
|
||||
:param conn: HTTP connection to be managed
|
||||
:type conn: cheroot.server.HTTPConnection
|
||||
"""
|
||||
conn.last_used = time.time()
|
||||
# if this conn doesn't have any more data waiting to be read,
|
||||
# register it with the selector.
|
||||
if conn.rfile.has_data():
|
||||
self.server.process_conn(conn)
|
||||
else:
|
||||
self._selector.register(
|
||||
conn.socket.fileno(), selectors.EVENT_READ, data=conn,
|
||||
)
|
||||
|
||||
def _expire(self, threshold):
|
||||
r"""Expire least recently used connections.
|
||||
|
||||
:param threshold: Connections that have not been used within this \
|
||||
duration (in seconds), are considered expired and \
|
||||
are closed and removed.
|
||||
:type threshold: float
|
||||
|
||||
This should be called periodically.
|
||||
"""
|
||||
# find any connections still registered with the selector
|
||||
# that have not been active recently enough.
|
||||
timed_out_connections = [
|
||||
(sock_fd, conn)
|
||||
for (sock_fd, conn) in self._selector.connections
|
||||
if conn != self.server and conn.last_used < threshold
|
||||
]
|
||||
for sock_fd, conn in timed_out_connections:
|
||||
self._selector.unregister(sock_fd)
|
||||
conn.close()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the selector loop in run() synchronously.
|
||||
|
||||
May take up to half a second.
|
||||
"""
|
||||
self._stop_requested = True
|
||||
while self._serving:
|
||||
time.sleep(0.01)
|
||||
|
||||
def run(self, expiration_interval):
|
||||
"""Run the connections selector indefinitely.
|
||||
|
||||
Args:
|
||||
expiration_interval (float): Interval, in seconds, at which
|
||||
connections will be checked for expiration.
|
||||
|
||||
Connections that are ready to process are submitted via
|
||||
self.server.process_conn()
|
||||
|
||||
Connections submitted for processing must be `put()`
|
||||
back if they should be examined again for another request.
|
||||
|
||||
Can be shut down by calling `stop()`.
|
||||
"""
|
||||
self._serving = True
|
||||
try:
|
||||
self._run(expiration_interval)
|
||||
finally:
|
||||
self._serving = False
|
||||
|
||||
def _run(self, expiration_interval):
|
||||
r"""Run connection handler loop until stop was requested.
|
||||
|
||||
:param expiration_interval: Interval, in seconds, at which \
|
||||
connections will be checked for \
|
||||
expiration.
|
||||
:type expiration_interval: float
|
||||
|
||||
Use ``expiration_interval`` as ``select()`` timeout
|
||||
to assure expired connections are closed in time.
|
||||
|
||||
On Windows cap the timeout to 0.05 seconds
|
||||
as ``select()`` does not return when a socket is ready.
|
||||
"""
|
||||
last_expiration_check = time.time()
|
||||
if IS_WINDOWS:
|
||||
# 0.05 seconds are used as an empirically obtained balance between
|
||||
# max connection delay and idle system load. Benchmarks show a
|
||||
# mean processing time per connection of ~0.03 seconds on Linux
|
||||
# and with 0.01 seconds timeout on Windows:
|
||||
# https://github.com/cherrypy/cheroot/pull/352
|
||||
# While this highly depends on system and hardware, 0.05 seconds
|
||||
# max delay should hence usually not significantly increase the
|
||||
# mean time/delay per connection, but significantly reduce idle
|
||||
# system load by reducing socket loops to 1/5 with 0.01 seconds.
|
||||
select_timeout = min(expiration_interval, 0.05)
|
||||
else:
|
||||
select_timeout = expiration_interval
|
||||
|
||||
while not self._stop_requested:
|
||||
try:
|
||||
active_list = self._selector.select(timeout=select_timeout)
|
||||
except OSError:
|
||||
self._remove_invalid_sockets()
|
||||
continue
|
||||
|
||||
for (sock_fd, conn) in active_list:
|
||||
if conn is self.server:
|
||||
# New connection
|
||||
new_conn = self._from_server_socket(self.server.socket)
|
||||
if new_conn is not None:
|
||||
self.server.process_conn(new_conn)
|
||||
else:
|
||||
# unregister connection from the selector until the server
|
||||
# has read from it and returned it via put()
|
||||
self._selector.unregister(sock_fd)
|
||||
self.server.process_conn(conn)
|
||||
|
||||
now = time.time()
|
||||
if (now - last_expiration_check) > expiration_interval:
|
||||
self._expire(threshold=now - self.server.timeout)
|
||||
last_expiration_check = now
|
||||
|
||||
def _remove_invalid_sockets(self):
|
||||
"""Clean up the resources of any broken connections.
|
||||
|
||||
This method attempts to detect any connections in an invalid state,
|
||||
unregisters them from the selector and closes the file descriptors of
|
||||
the corresponding network sockets where possible.
|
||||
"""
|
||||
invalid_conns = []
|
||||
for sock_fd, conn in self._selector.connections:
|
||||
if conn is self.server:
|
||||
continue
|
||||
|
||||
try:
|
||||
os.fstat(sock_fd)
|
||||
except OSError:
|
||||
invalid_conns.append((sock_fd, conn))
|
||||
|
||||
for sock_fd, conn in invalid_conns:
|
||||
self._selector.unregister(sock_fd)
|
||||
# One of the reason on why a socket could cause an error
|
||||
# is that the socket is already closed, ignore the
|
||||
# socket error if we try to close it at this point.
|
||||
with suppress(OSError):
|
||||
conn.close()
|
||||
|
||||
def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
|
||||
try:
|
||||
s, addr = server_socket.accept()
|
||||
if self.server.stats['Enabled']:
|
||||
self.server.stats['Accepts'] += 1
|
||||
prevent_socket_inheritance(s)
|
||||
if hasattr(s, 'settimeout'):
|
||||
s.settimeout(self.server.timeout)
|
||||
|
||||
mf = MakeFile
|
||||
ssl_env = {}
|
||||
# if ssl cert and key are set, we try to be a secure HTTP server
|
||||
if self.server.ssl_adapter is not None:
|
||||
try:
|
||||
s, ssl_env = self.server.ssl_adapter.wrap(s)
|
||||
except errors.NoSSLError:
|
||||
msg = (
|
||||
'The client sent a plain HTTP request, but '
|
||||
'this server only speaks HTTPS on this port.'
|
||||
)
|
||||
buf = [
|
||||
'%s 400 Bad Request\r\n' % self.server.protocol,
|
||||
'Content-Length: %s\r\n' % len(msg),
|
||||
'Content-Type: text/plain\r\n\r\n',
|
||||
msg,
|
||||
]
|
||||
|
||||
wfile = mf(s, 'wb', io.DEFAULT_BUFFER_SIZE)
|
||||
try:
|
||||
wfile.write(''.join(buf).encode('ISO-8859-1'))
|
||||
except OSError as ex:
|
||||
if ex.args[0] not in errors.socket_errors_to_ignore:
|
||||
raise
|
||||
return
|
||||
if not s:
|
||||
return
|
||||
mf = self.server.ssl_adapter.makefile
|
||||
# Re-apply our timeout since we may have a new socket object
|
||||
if hasattr(s, 'settimeout'):
|
||||
s.settimeout(self.server.timeout)
|
||||
|
||||
conn = self.server.ConnectionClass(self.server, s, mf)
|
||||
|
||||
if not isinstance(self.server.bind_addr, (str, bytes)):
|
||||
# optional values
|
||||
# Until we do DNS lookups, omit REMOTE_HOST
|
||||
if addr is None: # sometimes this can happen
|
||||
# figure out if AF_INET or AF_INET6.
|
||||
if len(s.getsockname()) == 2:
|
||||
# AF_INET
|
||||
addr = ('0.0.0.0', 0)
|
||||
else:
|
||||
# AF_INET6
|
||||
addr = ('::', 0)
|
||||
conn.remote_addr = addr[0]
|
||||
conn.remote_port = addr[1]
|
||||
|
||||
conn.ssl_env = ssl_env
|
||||
return conn
|
||||
|
||||
except socket.timeout:
|
||||
# The only reason for the timeout in start() is so we can
|
||||
# notice keyboard interrupts on Win32, which don't interrupt
|
||||
# accept() by default
|
||||
return
|
||||
except OSError as ex:
|
||||
if self.server.stats['Enabled']:
|
||||
self.server.stats['Socket Errors'] += 1
|
||||
if ex.args[0] in errors.socket_error_eintr:
|
||||
# I *think* this is right. EINTR should occur when a signal
|
||||
# is received during the accept() call; all docs say retry
|
||||
# the call, and I *think* I'm reading it right that Python
|
||||
# will then go ahead and poll for and handle the signal
|
||||
# elsewhere. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/707.
|
||||
return
|
||||
if ex.args[0] in errors.socket_errors_nonblocking:
|
||||
# Just try again. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/479.
|
||||
return
|
||||
if ex.args[0] in errors.socket_errors_to_ignore:
|
||||
# Our socket was closed.
|
||||
# See https://github.com/cherrypy/cherrypy/issues/686.
|
||||
return
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close all monitored connections."""
|
||||
for (_, conn) in self._selector.connections:
|
||||
if conn is not self.server: # server closes its own socket
|
||||
conn.close()
|
||||
self._selector.close()
|
||||
|
||||
@property
|
||||
def _num_connections(self):
|
||||
"""Return the current number of connections.
|
||||
|
||||
Includes all connections registered with the selector,
|
||||
minus one for the server socket, which is always registered
|
||||
with the selector.
|
||||
"""
|
||||
return len(self._selector) - 1
|
||||
|
||||
@property
|
||||
def can_add_keepalive_connection(self):
|
||||
"""Flag whether it is allowed to add a new keep-alive connection."""
|
||||
ka_limit = self.server.keep_alive_conn_limit
|
||||
return ka_limit is None or self._num_connections < ka_limit
|
||||
@ -0,0 +1,23 @@
|
||||
from typing import Any
|
||||
|
||||
def prevent_socket_inheritance(sock) -> None: ...
|
||||
|
||||
class _ThreadsafeSelector:
|
||||
def __init__(self) -> None: ...
|
||||
def __len__(self): ...
|
||||
@property
|
||||
def connections(self) -> None: ...
|
||||
def register(self, fileobj, events, data: Any | None = ...): ...
|
||||
def unregister(self, fileobj): ...
|
||||
def select(self, timeout: Any | None = ...): ...
|
||||
def close(self) -> None: ...
|
||||
|
||||
class ConnectionManager:
|
||||
server: Any
|
||||
def __init__(self, server) -> None: ...
|
||||
def put(self, conn) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
def run(self, expiration_interval) -> None: ...
|
||||
def close(self) -> None: ...
|
||||
@property
|
||||
def can_add_keepalive_connection(self): ...
|
||||
80
awesome_venv/lib/python3.10/site-packages/cheroot/errors.py
Normal file
80
awesome_venv/lib/python3.10/site-packages/cheroot/errors.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Collection of exceptions raised and/or processed by Cheroot."""
|
||||
|
||||
import errno
|
||||
import sys
|
||||
|
||||
|
||||
class MaxSizeExceeded(Exception):
|
||||
"""Exception raised when a client sends more data then allowed under limit.
|
||||
|
||||
Depends on ``request.body.maxbytes`` config option if used within CherryPy.
|
||||
"""
|
||||
|
||||
|
||||
class NoSSLError(Exception):
|
||||
"""Exception raised when a client speaks HTTP to an HTTPS socket."""
|
||||
|
||||
|
||||
class FatalSSLAlert(Exception):
|
||||
"""Exception raised when the SSL implementation signals a fatal alert."""
|
||||
|
||||
|
||||
def plat_specific_errors(*errnames):
|
||||
"""Return error numbers for all errors in ``errnames`` on this platform.
|
||||
|
||||
The :py:mod:`errno` module contains different global constants
|
||||
depending on the specific platform (OS). This function will return
|
||||
the list of numeric values for a given list of potential names.
|
||||
"""
|
||||
missing_attr = {None}
|
||||
unique_nums = {getattr(errno, k, None) for k in errnames}
|
||||
return list(unique_nums - missing_attr)
|
||||
|
||||
|
||||
socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR')
|
||||
|
||||
socket_errors_to_ignore = plat_specific_errors(
|
||||
'EPIPE',
|
||||
'EBADF', 'WSAEBADF',
|
||||
'ENOTSOCK', 'WSAENOTSOCK',
|
||||
'ETIMEDOUT', 'WSAETIMEDOUT',
|
||||
'ECONNREFUSED', 'WSAECONNREFUSED',
|
||||
'ECONNRESET', 'WSAECONNRESET',
|
||||
'ECONNABORTED', 'WSAECONNABORTED',
|
||||
'ENETRESET', 'WSAENETRESET',
|
||||
'EHOSTDOWN', 'EHOSTUNREACH',
|
||||
)
|
||||
socket_errors_to_ignore.append('timed out')
|
||||
socket_errors_to_ignore.append('The read operation timed out')
|
||||
socket_errors_nonblocking = plat_specific_errors(
|
||||
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK',
|
||||
)
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE'))
|
||||
socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE'))
|
||||
|
||||
|
||||
acceptable_sock_shutdown_error_codes = {
|
||||
errno.ENOTCONN,
|
||||
errno.EPIPE, errno.ESHUTDOWN, # corresponds to BrokenPipeError in Python 3
|
||||
errno.ECONNRESET, # corresponds to ConnectionResetError in Python 3
|
||||
}
|
||||
"""Errors that may happen during the connection close sequence.
|
||||
|
||||
* ENOTCONN — client is no longer connected
|
||||
* EPIPE — write on a pipe while the other end has been closed
|
||||
* ESHUTDOWN — write on a socket which has been shutdown for writing
|
||||
* ECONNRESET — connection is reset by the peer, we received a TCP RST packet
|
||||
|
||||
Refs:
|
||||
* https://github.com/cherrypy/cheroot/issues/341#issuecomment-735884889
|
||||
* https://bugs.python.org/issue30319
|
||||
* https://bugs.python.org/issue30329
|
||||
* https://github.com/python/cpython/commit/83a2c28
|
||||
* https://github.com/python/cpython/blob/c39b52f/Lib/poplib.py#L297-L302
|
||||
* https://docs.microsoft.com/windows/win32/api/winsock/nf-winsock-shutdown
|
||||
"""
|
||||
|
||||
acceptable_sock_shutdown_exceptions = (BrokenPipeError, ConnectionResetError)
|
||||
13
awesome_venv/lib/python3.10/site-packages/cheroot/errors.pyi
Normal file
13
awesome_venv/lib/python3.10/site-packages/cheroot/errors.pyi
Normal file
@ -0,0 +1,13 @@
|
||||
from typing import List, Set, Tuple, Type
|
||||
|
||||
class MaxSizeExceeded(Exception): ...
|
||||
class NoSSLError(Exception): ...
|
||||
class FatalSSLAlert(Exception): ...
|
||||
|
||||
def plat_specific_errors(*errnames: str) -> List[int]: ...
|
||||
|
||||
socket_error_eintr: List[int]
|
||||
socket_errors_to_ignore: List[int]
|
||||
socket_errors_nonblocking: List[int]
|
||||
acceptable_sock_shutdown_error_codes: Set[int]
|
||||
acceptable_sock_shutdown_exceptions: Tuple[Type[Exception], ...]
|
||||
@ -0,0 +1,76 @@
|
||||
"""Socket file object."""
|
||||
|
||||
import socket
|
||||
|
||||
# prefer slower Python-based io module
|
||||
import _pyio as io
|
||||
|
||||
|
||||
# Write only 16K at a time to sockets
|
||||
SOCK_WRITE_BLOCKSIZE = 16384
|
||||
|
||||
|
||||
class BufferedWriter(io.BufferedWriter):
|
||||
"""Faux file object attached to a socket object."""
|
||||
|
||||
def write(self, b):
|
||||
"""Write bytes to buffer."""
|
||||
self._checkClosed()
|
||||
if isinstance(b, str):
|
||||
raise TypeError("can't write str to binary stream")
|
||||
|
||||
with self._write_lock:
|
||||
self._write_buf.extend(b)
|
||||
self._flush_unlocked()
|
||||
return len(b)
|
||||
|
||||
def _flush_unlocked(self):
|
||||
self._checkClosed('flush of closed file')
|
||||
while self._write_buf:
|
||||
try:
|
||||
# ssl sockets only except 'bytes', not bytearrays
|
||||
# so perhaps we should conditionally wrap this for perf?
|
||||
n = self.raw.write(bytes(self._write_buf))
|
||||
except io.BlockingIOError as e:
|
||||
n = e.characters_written
|
||||
del self._write_buf[:n]
|
||||
|
||||
|
||||
class StreamReader(io.BufferedReader):
|
||||
"""Socket stream reader."""
|
||||
|
||||
def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
|
||||
"""Initialize socket stream reader."""
|
||||
super().__init__(socket.SocketIO(sock, mode), bufsize)
|
||||
self.bytes_read = 0
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
"""Capture bytes read."""
|
||||
val = super().read(*args, **kwargs)
|
||||
self.bytes_read += len(val)
|
||||
return val
|
||||
|
||||
def has_data(self):
|
||||
"""Return true if there is buffered data to read."""
|
||||
return len(self._read_buf) > self._read_pos
|
||||
|
||||
|
||||
class StreamWriter(BufferedWriter):
|
||||
"""Socket stream writer."""
|
||||
|
||||
def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE):
|
||||
"""Initialize socket stream writer."""
|
||||
super().__init__(socket.SocketIO(sock, mode), bufsize)
|
||||
self.bytes_written = 0
|
||||
|
||||
def write(self, val, *args, **kwargs):
|
||||
"""Capture bytes written."""
|
||||
res = super().write(val, *args, **kwargs)
|
||||
self.bytes_written += len(val)
|
||||
return res
|
||||
|
||||
|
||||
def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
|
||||
"""File object attached to a socket object."""
|
||||
cls = StreamReader if 'r' in mode else StreamWriter
|
||||
return cls(sock, mode, bufsize)
|
||||
@ -0,0 +1,19 @@
|
||||
import io
|
||||
|
||||
SOCK_WRITE_BLOCKSIZE: int
|
||||
|
||||
class BufferedWriter(io.BufferedWriter):
|
||||
def write(self, b): ...
|
||||
|
||||
class StreamReader(io.BufferedReader):
|
||||
bytes_read: int
|
||||
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
|
||||
def read(self, *args, **kwargs): ...
|
||||
def has_data(self): ...
|
||||
|
||||
class StreamWriter(BufferedWriter):
|
||||
bytes_written: int
|
||||
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
|
||||
def write(self, val, *args, **kwargs): ...
|
||||
|
||||
def MakeFile(sock, mode: str = ..., bufsize=...): ...
|
||||
2220
awesome_venv/lib/python3.10/site-packages/cheroot/server.py
Normal file
2220
awesome_venv/lib/python3.10/site-packages/cheroot/server.py
Normal file
File diff suppressed because it is too large
Load Diff
175
awesome_venv/lib/python3.10/site-packages/cheroot/server.pyi
Normal file
175
awesome_venv/lib/python3.10/site-packages/cheroot/server.pyi
Normal file
@ -0,0 +1,175 @@
|
||||
from typing import Any
|
||||
|
||||
class HeaderReader:
|
||||
def __call__(self, rfile, hdict: Any | None = ...): ...
|
||||
|
||||
class DropUnderscoreHeaderReader(HeaderReader): ...
|
||||
|
||||
class SizeCheckWrapper:
|
||||
rfile: Any
|
||||
maxlen: Any
|
||||
bytes_read: int
|
||||
def __init__(self, rfile, maxlen) -> None: ...
|
||||
def read(self, size: Any | None = ...): ...
|
||||
def readline(self, size: Any | None = ...): ...
|
||||
def readlines(self, sizehint: int = ...): ...
|
||||
def close(self) -> None: ...
|
||||
def __iter__(self): ...
|
||||
def __next__(self): ...
|
||||
next: Any
|
||||
|
||||
class KnownLengthRFile:
|
||||
rfile: Any
|
||||
remaining: Any
|
||||
def __init__(self, rfile, content_length) -> None: ...
|
||||
def read(self, size: Any | None = ...): ...
|
||||
def readline(self, size: Any | None = ...): ...
|
||||
def readlines(self, sizehint: int = ...): ...
|
||||
def close(self) -> None: ...
|
||||
def __iter__(self): ...
|
||||
def __next__(self): ...
|
||||
next: Any
|
||||
|
||||
class ChunkedRFile:
|
||||
rfile: Any
|
||||
maxlen: Any
|
||||
bytes_read: int
|
||||
buffer: Any
|
||||
bufsize: Any
|
||||
closed: bool
|
||||
def __init__(self, rfile, maxlen, bufsize: int = ...) -> None: ...
|
||||
def read(self, size: Any | None = ...): ...
|
||||
def readline(self, size: Any | None = ...): ...
|
||||
def readlines(self, sizehint: int = ...): ...
|
||||
def read_trailer_lines(self) -> None: ...
|
||||
def close(self) -> None: ...
|
||||
|
||||
class HTTPRequest:
|
||||
server: Any
|
||||
conn: Any
|
||||
inheaders: Any
|
||||
outheaders: Any
|
||||
ready: bool
|
||||
close_connection: bool
|
||||
chunked_write: bool
|
||||
header_reader: Any
|
||||
started_request: bool
|
||||
scheme: bytes
|
||||
response_protocol: str
|
||||
status: str
|
||||
sent_headers: bool
|
||||
chunked_read: bool
|
||||
proxy_mode: Any
|
||||
strict_mode: Any
|
||||
def __init__(self, server, conn, proxy_mode: bool = ..., strict_mode: bool = ...) -> None: ...
|
||||
rfile: Any
|
||||
def parse_request(self) -> None: ...
|
||||
uri: Any
|
||||
method: Any
|
||||
authority: Any
|
||||
path: Any
|
||||
qs: Any
|
||||
request_protocol: Any
|
||||
def read_request_line(self): ...
|
||||
def read_request_headers(self): ...
|
||||
def respond(self) -> None: ...
|
||||
def simple_response(self, status, msg: str = ...) -> None: ...
|
||||
def ensure_headers_sent(self) -> None: ...
|
||||
def write(self, chunk) -> None: ...
|
||||
def send_headers(self) -> None: ...
|
||||
|
||||
class HTTPConnection:
|
||||
remote_addr: Any
|
||||
remote_port: Any
|
||||
ssl_env: Any
|
||||
rbufsize: Any
|
||||
wbufsize: Any
|
||||
RequestHandlerClass: Any
|
||||
peercreds_enabled: bool
|
||||
peercreds_resolve_enabled: bool
|
||||
last_used: Any
|
||||
server: Any
|
||||
socket: Any
|
||||
rfile: Any
|
||||
wfile: Any
|
||||
requests_seen: int
|
||||
def __init__(self, server, sock, makefile=...) -> None: ...
|
||||
def communicate(self): ...
|
||||
linger: bool
|
||||
def close(self) -> None: ...
|
||||
def get_peer_creds(self): ...
|
||||
@property
|
||||
def peer_pid(self): ...
|
||||
@property
|
||||
def peer_uid(self): ...
|
||||
@property
|
||||
def peer_gid(self): ...
|
||||
def resolve_peer_creds(self): ...
|
||||
@property
|
||||
def peer_user(self): ...
|
||||
@property
|
||||
def peer_group(self): ...
|
||||
|
||||
class HTTPServer:
|
||||
gateway: Any
|
||||
minthreads: Any
|
||||
maxthreads: Any
|
||||
server_name: Any
|
||||
protocol: str
|
||||
request_queue_size: int
|
||||
shutdown_timeout: int
|
||||
timeout: int
|
||||
expiration_interval: float
|
||||
version: Any
|
||||
software: Any
|
||||
ready: bool
|
||||
max_request_header_size: int
|
||||
max_request_body_size: int
|
||||
nodelay: bool
|
||||
ConnectionClass: Any
|
||||
ssl_adapter: Any
|
||||
peercreds_enabled: bool
|
||||
peercreds_resolve_enabled: bool
|
||||
reuse_port: bool
|
||||
keep_alive_conn_limit: int
|
||||
requests: Any
|
||||
def __init__(self, bind_addr, gateway, minthreads: int = ..., maxthreads: int = ..., server_name: Any | None = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ..., reuse_port: bool = ...) -> None: ...
|
||||
stats: Any
|
||||
def clear_stats(self): ...
|
||||
def runtime(self): ...
|
||||
@property
|
||||
def bind_addr(self): ...
|
||||
@bind_addr.setter
|
||||
def bind_addr(self, value) -> None: ...
|
||||
def safe_start(self) -> None: ...
|
||||
socket: Any
|
||||
def prepare(self) -> None: ...
|
||||
def serve(self) -> None: ...
|
||||
def start(self) -> None: ...
|
||||
@property
|
||||
def can_add_keepalive_connection(self): ...
|
||||
def put_conn(self, conn) -> None: ...
|
||||
def error_log(self, msg: str = ..., level: int = ..., traceback: bool = ...) -> None: ...
|
||||
def bind(self, family, type, proto: int = ...): ...
|
||||
def bind_unix_socket(self, bind_addr): ...
|
||||
@staticmethod
|
||||
def _make_socket_reusable(socket_, bind_addr) -> None: ...
|
||||
@classmethod
|
||||
def prepare_socket(cls, bind_addr, family, type, proto, nodelay, ssl_adapter, reuse_port: bool = ...): ...
|
||||
@staticmethod
|
||||
def bind_socket(socket_, bind_addr): ...
|
||||
@staticmethod
|
||||
def resolve_real_bind_addr(socket_): ...
|
||||
def process_conn(self, conn) -> None: ...
|
||||
@property
|
||||
def interrupt(self): ...
|
||||
@interrupt.setter
|
||||
def interrupt(self, interrupt) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
|
||||
class Gateway:
|
||||
req: Any
|
||||
def __init__(self, req) -> None: ...
|
||||
def respond(self) -> None: ...
|
||||
|
||||
def get_ssl_adapter_class(name: str = ...): ...
|
||||
@ -0,0 +1,46 @@
|
||||
"""Implementation of the SSL adapter base interface."""
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class Adapter(metaclass=ABCMeta):
|
||||
"""Base class for SSL driver library adapters.
|
||||
|
||||
Required methods:
|
||||
|
||||
* ``wrap(sock) -> (wrapped socket, ssl environ dict)``
|
||||
* ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
|
||||
socket file object``
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __init__(
|
||||
self, certificate, private_key, certificate_chain=None,
|
||||
ciphers=None,
|
||||
):
|
||||
"""Set up certificates, private key ciphers and reset context."""
|
||||
self.certificate = certificate
|
||||
self.private_key = private_key
|
||||
self.certificate_chain = certificate_chain
|
||||
self.ciphers = ciphers
|
||||
self.context = None
|
||||
|
||||
@abstractmethod
|
||||
def bind(self, sock):
|
||||
"""Wrap and return the given socket."""
|
||||
return sock
|
||||
|
||||
@abstractmethod
|
||||
def wrap(self, sock):
|
||||
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def get_environ(self):
|
||||
"""Return WSGI environ entries to be merged into each request."""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def makefile(self, sock, mode='r', bufsize=-1):
|
||||
"""Return socket file object."""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
@ -0,0 +1,19 @@
|
||||
from abc import abstractmethod, ABCMeta
|
||||
from typing import Any
|
||||
|
||||
class Adapter(metaclass=ABCMeta):
|
||||
certificate: Any
|
||||
private_key: Any
|
||||
certificate_chain: Any
|
||||
ciphers: Any
|
||||
context: Any
|
||||
@abstractmethod
|
||||
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...): ...
|
||||
@abstractmethod
|
||||
def bind(self, sock): ...
|
||||
@abstractmethod
|
||||
def wrap(self, sock): ...
|
||||
@abstractmethod
|
||||
def get_environ(self): ...
|
||||
@abstractmethod
|
||||
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
478
awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.py
Normal file
478
awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.py
Normal file
@ -0,0 +1,478 @@
|
||||
"""
|
||||
A library for integrating Python's builtin :py:mod:`ssl` library with Cheroot.
|
||||
|
||||
The :py:mod:`ssl` module must be importable for SSL functionality.
|
||||
|
||||
To use this module, set ``HTTPServer.ssl_adapter`` to an instance of
|
||||
``BuiltinSSLAdapter``.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
try:
|
||||
from _pyio import DEFAULT_BUFFER_SIZE
|
||||
except ImportError:
|
||||
try:
|
||||
from io import DEFAULT_BUFFER_SIZE
|
||||
except ImportError:
|
||||
DEFAULT_BUFFER_SIZE = -1
|
||||
|
||||
from . import Adapter
|
||||
from .. import errors
|
||||
from .._compat import IS_ABOVE_OPENSSL10
|
||||
from ..makefile import StreamReader, StreamWriter
|
||||
from ..server import HTTPServer
|
||||
|
||||
generic_socket_error = OSError
|
||||
|
||||
|
||||
def _assert_ssl_exc_contains(exc, *msgs):
|
||||
"""Check whether SSL exception contains either of messages provided."""
|
||||
if len(msgs) < 1:
|
||||
raise TypeError(
|
||||
'_assert_ssl_exc_contains() requires '
|
||||
'at least one message to be passed.',
|
||||
)
|
||||
err_msg_lower = str(exc).lower()
|
||||
return any(m.lower() in err_msg_lower for m in msgs)
|
||||
|
||||
|
||||
def _loopback_for_cert_thread(context, server):
|
||||
"""Wrap a socket in ssl and perform the server-side handshake."""
|
||||
# As we only care about parsing the certificate, the failure of
|
||||
# which will cause an exception in ``_loopback_for_cert``,
|
||||
# we can safely ignore connection and ssl related exceptions. Ref:
|
||||
# https://github.com/cherrypy/cheroot/issues/302#issuecomment-662592030
|
||||
with suppress(ssl.SSLError, OSError):
|
||||
with context.wrap_socket(
|
||||
server, do_handshake_on_connect=True, server_side=True,
|
||||
) as ssl_sock:
|
||||
# in TLS 1.3 (Python 3.7+, OpenSSL 1.1.1+), the server
|
||||
# sends the client session tickets that can be used to
|
||||
# resume the TLS session on a new connection without
|
||||
# performing the full handshake again. session tickets are
|
||||
# sent as a post-handshake message at some _unspecified_
|
||||
# time and thus a successful connection may be closed
|
||||
# without the client having received the tickets.
|
||||
# Unfortunately, on Windows (Python 3.8+), this is treated
|
||||
# as an incomplete handshake on the server side and a
|
||||
# ``ConnectionAbortedError`` is raised.
|
||||
# TLS 1.3 support is still incomplete in Python 3.8;
|
||||
# there is no way for the client to wait for tickets.
|
||||
# While not necessary for retrieving the parsed certificate,
|
||||
# we send a tiny bit of data over the connection in an
|
||||
# attempt to give the server a chance to send the session
|
||||
# tickets and close the connection cleanly.
|
||||
# Note that, as this is essentially a race condition,
|
||||
# the error may still occur ocasionally.
|
||||
ssl_sock.send(b'0000')
|
||||
|
||||
|
||||
def _loopback_for_cert(certificate, private_key, certificate_chain):
|
||||
"""Create a loopback connection to parse a cert with a private key."""
|
||||
context = ssl.create_default_context(cafile=certificate_chain)
|
||||
context.load_cert_chain(certificate, private_key)
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# Python 3+ Unix, Python 3.5+ Windows
|
||||
client, server = socket.socketpair()
|
||||
try:
|
||||
# `wrap_socket` will block until the ssl handshake is complete.
|
||||
# it must be called on both ends at the same time -> thread
|
||||
# openssl will cache the peer's cert during a successful handshake
|
||||
# and return it via `getpeercert` even after the socket is closed.
|
||||
# when `close` is called, the SSL shutdown notice will be sent
|
||||
# and then python will wait to receive the corollary shutdown.
|
||||
thread = threading.Thread(
|
||||
target=_loopback_for_cert_thread, args=(context, server),
|
||||
)
|
||||
try:
|
||||
thread.start()
|
||||
with context.wrap_socket(
|
||||
client, do_handshake_on_connect=True,
|
||||
server_side=False,
|
||||
) as ssl_sock:
|
||||
ssl_sock.recv(4)
|
||||
return ssl_sock.getpeercert()
|
||||
finally:
|
||||
thread.join()
|
||||
finally:
|
||||
client.close()
|
||||
server.close()
|
||||
|
||||
|
||||
def _parse_cert(certificate, private_key, certificate_chain):
|
||||
"""Parse a certificate."""
|
||||
# loopback_for_cert uses socket.socketpair which was only
|
||||
# introduced in Python 3.0 for *nix and 3.5 for Windows
|
||||
# and requires OS support (AttributeError, OSError)
|
||||
# it also requires a private key either in its own file
|
||||
# or combined with the cert (SSLError)
|
||||
with suppress(AttributeError, ssl.SSLError, OSError):
|
||||
return _loopback_for_cert(certificate, private_key, certificate_chain)
|
||||
|
||||
# KLUDGE: using an undocumented, private, test method to parse a cert
|
||||
# unfortunately, it is the only built-in way without a connection
|
||||
# as a private, undocumented method, it may change at any time
|
||||
# so be tolerant of *any* possible errors it may raise
|
||||
with suppress(Exception):
|
||||
return ssl._ssl._test_decode_cert(certificate)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _sni_callback(sock, sni, context):
|
||||
"""Handle the SNI callback to tag the socket with the SNI."""
|
||||
sock.sni = sni
|
||||
# return None to allow the TLS negotiation to continue
|
||||
|
||||
|
||||
class BuiltinSSLAdapter(Adapter):
|
||||
"""Wrapper for integrating Python's builtin :py:mod:`ssl` with Cheroot."""
|
||||
|
||||
certificate = None
|
||||
"""The file name of the server SSL certificate."""
|
||||
|
||||
private_key = None
|
||||
"""The file name of the server's private key file."""
|
||||
|
||||
certificate_chain = None
|
||||
"""The file name of the certificate chain file."""
|
||||
|
||||
ciphers = None
|
||||
"""The ciphers list of SSL."""
|
||||
|
||||
# from mod_ssl/pkg.sslmod/ssl_engine_vars.c ssl_var_lookup_ssl_cert
|
||||
CERT_KEY_TO_ENV = {
|
||||
'version': 'M_VERSION',
|
||||
'serialNumber': 'M_SERIAL',
|
||||
'notBefore': 'V_START',
|
||||
'notAfter': 'V_END',
|
||||
'subject': 'S_DN',
|
||||
'issuer': 'I_DN',
|
||||
'subjectAltName': 'SAN',
|
||||
# not parsed by the Python standard library
|
||||
# - A_SIG
|
||||
# - A_KEY
|
||||
# not provided by mod_ssl
|
||||
# - OCSP
|
||||
# - caIssuers
|
||||
# - crlDistributionPoints
|
||||
}
|
||||
|
||||
# from mod_ssl/pkg.sslmod/ssl_engine_vars.c ssl_var_lookup_ssl_cert_dn_rec
|
||||
CERT_KEY_TO_LDAP_CODE = {
|
||||
'countryName': 'C',
|
||||
'stateOrProvinceName': 'ST',
|
||||
# NOTE: mod_ssl also provides 'stateOrProvinceName' as 'SP'
|
||||
# for compatibility with SSLeay
|
||||
'localityName': 'L',
|
||||
'organizationName': 'O',
|
||||
'organizationalUnitName': 'OU',
|
||||
'commonName': 'CN',
|
||||
'title': 'T',
|
||||
'initials': 'I',
|
||||
'givenName': 'G',
|
||||
'surname': 'S',
|
||||
'description': 'D',
|
||||
'userid': 'UID',
|
||||
'emailAddress': 'Email',
|
||||
# not provided by mod_ssl
|
||||
# - dnQualifier: DNQ
|
||||
# - domainComponent: DC
|
||||
# - postalCode: PC
|
||||
# - streetAddress: STREET
|
||||
# - serialNumber
|
||||
# - generationQualifier
|
||||
# - pseudonym
|
||||
# - jurisdictionCountryName
|
||||
# - jurisdictionLocalityName
|
||||
# - jurisdictionStateOrProvince
|
||||
# - businessCategory
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, certificate, private_key, certificate_chain=None,
|
||||
ciphers=None,
|
||||
):
|
||||
"""Set up context in addition to base class properties if available."""
|
||||
if ssl is None:
|
||||
raise ImportError('You must install the ssl module to use HTTPS.')
|
||||
|
||||
super(BuiltinSSLAdapter, self).__init__(
|
||||
certificate, private_key, certificate_chain, ciphers,
|
||||
)
|
||||
|
||||
self.context = ssl.create_default_context(
|
||||
purpose=ssl.Purpose.CLIENT_AUTH,
|
||||
cafile=certificate_chain,
|
||||
)
|
||||
self.context.load_cert_chain(certificate, private_key)
|
||||
if self.ciphers is not None:
|
||||
self.context.set_ciphers(ciphers)
|
||||
|
||||
self._server_env = self._make_env_cert_dict(
|
||||
'SSL_SERVER',
|
||||
_parse_cert(certificate, private_key, self.certificate_chain),
|
||||
)
|
||||
if not self._server_env:
|
||||
return
|
||||
cert = None
|
||||
with open(certificate, mode='rt') as f:
|
||||
cert = f.read()
|
||||
|
||||
# strip off any keys by only taking the first certificate
|
||||
cert_start = cert.find(ssl.PEM_HEADER)
|
||||
if cert_start == -1:
|
||||
return
|
||||
cert_end = cert.find(ssl.PEM_FOOTER, cert_start)
|
||||
if cert_end == -1:
|
||||
return
|
||||
cert_end += len(ssl.PEM_FOOTER)
|
||||
self._server_env['SSL_SERVER_CERT'] = cert[cert_start:cert_end]
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
""":py:class:`~ssl.SSLContext` that will be used to wrap sockets."""
|
||||
return self._context
|
||||
|
||||
@context.setter
|
||||
def context(self, context):
|
||||
"""Set the ssl ``context`` to use."""
|
||||
self._context = context
|
||||
# Python 3.7+
|
||||
# if a context is provided via `cherrypy.config.update` then
|
||||
# `self.context` will be set after `__init__`
|
||||
# use a property to intercept it to add an SNI callback
|
||||
# but don't override the user's callback
|
||||
# TODO: chain callbacks
|
||||
with suppress(AttributeError):
|
||||
if ssl.HAS_SNI and context.sni_callback is None:
|
||||
context.sni_callback = _sni_callback
|
||||
|
||||
def bind(self, sock):
|
||||
"""Wrap and return the given socket."""
|
||||
return super(BuiltinSSLAdapter, self).bind(sock)
|
||||
|
||||
def wrap(self, sock):
|
||||
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||||
EMPTY_RESULT = None, {}
|
||||
try:
|
||||
s = self.context.wrap_socket(
|
||||
sock, do_handshake_on_connect=True, server_side=True,
|
||||
)
|
||||
except ssl.SSLError as ex:
|
||||
if ex.errno == ssl.SSL_ERROR_EOF:
|
||||
# This is almost certainly due to the cherrypy engine
|
||||
# 'pinging' the socket to assert it's connectable;
|
||||
# the 'ping' isn't SSL.
|
||||
return EMPTY_RESULT
|
||||
elif ex.errno == ssl.SSL_ERROR_SSL:
|
||||
if _assert_ssl_exc_contains(ex, 'http request'):
|
||||
# The client is speaking HTTP to an HTTPS server.
|
||||
raise errors.NoSSLError
|
||||
|
||||
# Check if it's one of the known errors
|
||||
# Errors that are caught by PyOpenSSL, but thrown by
|
||||
# built-in ssl
|
||||
_block_errors = (
|
||||
'unknown protocol', 'unknown ca', 'unknown_ca',
|
||||
'unknown error',
|
||||
'https proxy request', 'inappropriate fallback',
|
||||
'wrong version number',
|
||||
'no shared cipher', 'certificate unknown',
|
||||
'ccs received early',
|
||||
'certificate verify failed', # client cert w/o trusted CA
|
||||
'version too low', # caused by SSL3 connections
|
||||
'unsupported protocol', # caused by TLS1 connections
|
||||
)
|
||||
if _assert_ssl_exc_contains(ex, *_block_errors):
|
||||
# Accepted error, let's pass
|
||||
return EMPTY_RESULT
|
||||
elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'):
|
||||
# This error is thrown by builtin SSL after a timeout
|
||||
# when client is speaking HTTP to an HTTPS server.
|
||||
# The connection can safely be dropped.
|
||||
return EMPTY_RESULT
|
||||
raise
|
||||
except generic_socket_error as exc:
|
||||
"""It is unclear why exactly this happens.
|
||||
|
||||
It's reproducible only with openssl>1.0 and stdlib
|
||||
:py:mod:`ssl` wrapper.
|
||||
In CherryPy it's triggered by Checker plugin, which connects
|
||||
to the app listening to the socket port in TLS mode via plain
|
||||
HTTP during startup (from the same process).
|
||||
|
||||
|
||||
Ref: https://github.com/cherrypy/cherrypy/issues/1618
|
||||
"""
|
||||
is_error0 = exc.args == (0, 'Error')
|
||||
|
||||
if is_error0 and IS_ABOVE_OPENSSL10:
|
||||
return EMPTY_RESULT
|
||||
raise
|
||||
return s, self.get_environ(s)
|
||||
|
||||
def get_environ(self, sock):
|
||||
"""Create WSGI environ entries to be merged into each request."""
|
||||
cipher = sock.cipher()
|
||||
ssl_environ = {
|
||||
'wsgi.url_scheme': 'https',
|
||||
'HTTPS': 'on',
|
||||
'SSL_PROTOCOL': cipher[1],
|
||||
'SSL_CIPHER': cipher[0],
|
||||
'SSL_CIPHER_EXPORT': '',
|
||||
'SSL_CIPHER_USEKEYSIZE': cipher[2],
|
||||
'SSL_VERSION_INTERFACE': '%s Python/%s' % (
|
||||
HTTPServer.version, sys.version,
|
||||
),
|
||||
'SSL_VERSION_LIBRARY': ssl.OPENSSL_VERSION,
|
||||
'SSL_CLIENT_VERIFY': 'NONE',
|
||||
# 'NONE' - client did not provide a cert (overriden below)
|
||||
}
|
||||
|
||||
# Python 3.3+
|
||||
with suppress(AttributeError):
|
||||
compression = sock.compression()
|
||||
if compression is not None:
|
||||
ssl_environ['SSL_COMPRESS_METHOD'] = compression
|
||||
|
||||
# Python 3.6+
|
||||
with suppress(AttributeError):
|
||||
ssl_environ['SSL_SESSION_ID'] = sock.session.id.hex()
|
||||
with suppress(AttributeError):
|
||||
target_cipher = cipher[:2]
|
||||
for cip in sock.context.get_ciphers():
|
||||
if target_cipher == (cip['name'], cip['protocol']):
|
||||
ssl_environ['SSL_CIPHER_ALGKEYSIZE'] = cip['alg_bits']
|
||||
break
|
||||
|
||||
# Python 3.7+ sni_callback
|
||||
with suppress(AttributeError):
|
||||
ssl_environ['SSL_TLS_SNI'] = sock.sni
|
||||
|
||||
if self.context and self.context.verify_mode != ssl.CERT_NONE:
|
||||
client_cert = sock.getpeercert()
|
||||
if client_cert:
|
||||
# builtin ssl **ALWAYS** validates client certificates
|
||||
# and terminates the connection on failure
|
||||
ssl_environ['SSL_CLIENT_VERIFY'] = 'SUCCESS'
|
||||
ssl_environ.update(
|
||||
self._make_env_cert_dict('SSL_CLIENT', client_cert),
|
||||
)
|
||||
ssl_environ['SSL_CLIENT_CERT'] = ssl.DER_cert_to_PEM_cert(
|
||||
sock.getpeercert(binary_form=True),
|
||||
).strip()
|
||||
|
||||
ssl_environ.update(self._server_env)
|
||||
|
||||
# not supplied by the Python standard library (as of 3.8)
|
||||
# - SSL_SESSION_RESUMED
|
||||
# - SSL_SECURE_RENEG
|
||||
# - SSL_CLIENT_CERT_CHAIN_n
|
||||
# - SRP_USER
|
||||
# - SRP_USERINFO
|
||||
|
||||
return ssl_environ
|
||||
|
||||
def _make_env_cert_dict(self, env_prefix, parsed_cert):
|
||||
"""Return a dict of WSGI environment variables for a certificate.
|
||||
|
||||
E.g. SSL_CLIENT_M_VERSION, SSL_CLIENT_M_SERIAL, etc.
|
||||
See https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
|
||||
"""
|
||||
if not parsed_cert:
|
||||
return {}
|
||||
|
||||
env = {}
|
||||
for cert_key, env_var in self.CERT_KEY_TO_ENV.items():
|
||||
key = '%s_%s' % (env_prefix, env_var)
|
||||
value = parsed_cert.get(cert_key)
|
||||
if env_var == 'SAN':
|
||||
env.update(self._make_env_san_dict(key, value))
|
||||
elif env_var.endswith('_DN'):
|
||||
env.update(self._make_env_dn_dict(key, value))
|
||||
else:
|
||||
env[key] = str(value)
|
||||
|
||||
# mod_ssl 2.1+; Python 3.2+
|
||||
# number of days until the certificate expires
|
||||
if 'notBefore' in parsed_cert:
|
||||
remain = ssl.cert_time_to_seconds(parsed_cert['notAfter'])
|
||||
remain -= ssl.cert_time_to_seconds(parsed_cert['notBefore'])
|
||||
remain /= 60 * 60 * 24
|
||||
env['%s_V_REMAIN' % (env_prefix,)] = str(int(remain))
|
||||
|
||||
return env
|
||||
|
||||
def _make_env_san_dict(self, env_prefix, cert_value):
|
||||
"""Return a dict of WSGI environment variables for a certificate DN.
|
||||
|
||||
E.g. SSL_CLIENT_SAN_Email_0, SSL_CLIENT_SAN_DNS_0, etc.
|
||||
See SSL_CLIENT_SAN_* at
|
||||
https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
|
||||
"""
|
||||
if not cert_value:
|
||||
return {}
|
||||
|
||||
env = {}
|
||||
dns_count = 0
|
||||
email_count = 0
|
||||
for attr_name, val in cert_value:
|
||||
if attr_name == 'DNS':
|
||||
env['%s_DNS_%i' % (env_prefix, dns_count)] = val
|
||||
dns_count += 1
|
||||
elif attr_name == 'Email':
|
||||
env['%s_Email_%i' % (env_prefix, email_count)] = val
|
||||
email_count += 1
|
||||
|
||||
# other mod_ssl SAN vars:
|
||||
# - SAN_OTHER_msUPN_n
|
||||
return env
|
||||
|
||||
def _make_env_dn_dict(self, env_prefix, cert_value):
|
||||
"""Return a dict of WSGI environment variables for a certificate DN.
|
||||
|
||||
E.g. SSL_CLIENT_S_DN_CN, SSL_CLIENT_S_DN_C, etc.
|
||||
See SSL_CLIENT_S_DN_x509 at
|
||||
https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
|
||||
"""
|
||||
if not cert_value:
|
||||
return {}
|
||||
|
||||
dn = []
|
||||
dn_attrs = {}
|
||||
for rdn in cert_value:
|
||||
for attr_name, val in rdn:
|
||||
attr_code = self.CERT_KEY_TO_LDAP_CODE.get(attr_name)
|
||||
dn.append('%s=%s' % (attr_code or attr_name, val))
|
||||
if not attr_code:
|
||||
continue
|
||||
dn_attrs.setdefault(attr_code, [])
|
||||
dn_attrs[attr_code].append(val)
|
||||
|
||||
env = {
|
||||
env_prefix: ','.join(dn),
|
||||
}
|
||||
for attr_code, values in dn_attrs.items():
|
||||
env['%s_%s' % (env_prefix, attr_code)] = ','.join(values)
|
||||
if len(values) == 1:
|
||||
continue
|
||||
for i, val in enumerate(values):
|
||||
env['%s_%s_%i' % (env_prefix, attr_code, i)] = val
|
||||
return env
|
||||
|
||||
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
|
||||
"""Return socket file object."""
|
||||
cls = StreamReader if 'r' in mode else StreamWriter
|
||||
return cls(sock, mode, bufsize)
|
||||
@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
from . import Adapter
|
||||
|
||||
DEFAULT_BUFFER_SIZE: int
|
||||
|
||||
class BuiltinSSLAdapter(Adapter):
|
||||
CERT_KEY_TO_ENV: Any
|
||||
CERT_KEY_TO_LDAP_CODE: Any
|
||||
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
|
||||
@property
|
||||
def context(self): ...
|
||||
@context.setter
|
||||
def context(self, context) -> None: ...
|
||||
def bind(self, sock): ...
|
||||
def wrap(self, sock): ...
|
||||
def get_environ(self, sock): ...
|
||||
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
|
||||
@ -0,0 +1,376 @@
|
||||
"""
|
||||
A library for integrating :doc:`pyOpenSSL <pyopenssl:index>` with Cheroot.
|
||||
|
||||
The :py:mod:`OpenSSL <pyopenssl:OpenSSL>` module must be importable
|
||||
for SSL/TLS/HTTPS functionality.
|
||||
You can obtain it from `here <https://github.com/pyca/pyopenssl>`_.
|
||||
|
||||
To use this module, set :py:attr:`HTTPServer.ssl_adapter
|
||||
<cheroot.server.HTTPServer.ssl_adapter>` to an instance of
|
||||
:py:class:`ssl.Adapter <cheroot.ssl.Adapter>`.
|
||||
There are two ways to use :abbr:`TLS (Transport-Level Security)`:
|
||||
|
||||
Method One
|
||||
----------
|
||||
|
||||
* :py:attr:`ssl_adapter.context
|
||||
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.context>`: an instance of
|
||||
:py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`.
|
||||
|
||||
If this is not None, it is assumed to be an :py:class:`SSL.Context
|
||||
<pyopenssl:OpenSSL.SSL.Context>` instance, and will be passed to
|
||||
:py:class:`SSL.Connection <pyopenssl:OpenSSL.SSL.Connection>` on bind().
|
||||
The developer is responsible for forming a valid :py:class:`Context
|
||||
<pyopenssl:OpenSSL.SSL.Context>` object. This
|
||||
approach is to be preferred for more flexibility, e.g. if the cert and
|
||||
key are streams instead of files, or need decryption, or
|
||||
:py:data:`SSL.SSLv3_METHOD <pyopenssl:OpenSSL.SSL.SSLv3_METHOD>`
|
||||
is desired instead of the default :py:data:`SSL.SSLv23_METHOD
|
||||
<pyopenssl:OpenSSL.SSL.SSLv3_METHOD>`, etc. Consult
|
||||
the :doc:`pyOpenSSL <pyopenssl:api/ssl>` documentation for
|
||||
complete options.
|
||||
|
||||
Method Two (shortcut)
|
||||
---------------------
|
||||
|
||||
* :py:attr:`ssl_adapter.certificate
|
||||
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.certificate>`: the file name
|
||||
of the server's TLS certificate.
|
||||
* :py:attr:`ssl_adapter.private_key
|
||||
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.private_key>`: the file name
|
||||
of the server's private key file.
|
||||
|
||||
Both are :py:data:`None` by default. If :py:attr:`ssl_adapter.context
|
||||
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.context>` is :py:data:`None`,
|
||||
but ``.private_key`` and ``.certificate`` are both given and valid, they
|
||||
will be read, and the context will be automatically created from them.
|
||||
|
||||
.. spelling::
|
||||
|
||||
pyopenssl
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
import OpenSSL.version
|
||||
from OpenSSL import SSL
|
||||
from OpenSSL import crypto
|
||||
|
||||
try:
|
||||
ssl_conn_type = SSL.Connection
|
||||
except AttributeError:
|
||||
ssl_conn_type = SSL.ConnectionType
|
||||
except ImportError:
|
||||
SSL = None
|
||||
|
||||
from . import Adapter
|
||||
from .. import errors, server as cheroot_server
|
||||
from ..makefile import StreamReader, StreamWriter
|
||||
|
||||
|
||||
class SSLFileobjectMixin:
|
||||
"""Base mixin for a TLS socket stream."""
|
||||
|
||||
ssl_timeout = 3
|
||||
ssl_retry = .01
|
||||
|
||||
# FIXME:
|
||||
def _safe_call(self, is_reader, call, *args, **kwargs): # noqa: C901
|
||||
"""Wrap the given call with TLS error-trapping.
|
||||
|
||||
is_reader: if False EOF errors will be raised. If True, EOF errors
|
||||
will return "" (to emulate normal sockets).
|
||||
"""
|
||||
start = time.time()
|
||||
while True:
|
||||
try:
|
||||
return call(*args, **kwargs)
|
||||
except SSL.WantReadError:
|
||||
# Sleep and try again. This is dangerous, because it means
|
||||
# the rest of the stack has no way of differentiating
|
||||
# between a "new handshake" error and "client dropped".
|
||||
# Note this isn't an endless loop: there's a timeout below.
|
||||
# Ref: https://stackoverflow.com/a/5133568/595220
|
||||
time.sleep(self.ssl_retry)
|
||||
except SSL.WantWriteError:
|
||||
time.sleep(self.ssl_retry)
|
||||
except SSL.SysCallError as e:
|
||||
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||||
return b''
|
||||
|
||||
errnum = e.args[0]
|
||||
if is_reader and errnum in errors.socket_errors_to_ignore:
|
||||
return b''
|
||||
raise socket.error(errnum)
|
||||
except SSL.Error as e:
|
||||
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||||
return b''
|
||||
|
||||
thirdarg = None
|
||||
try:
|
||||
thirdarg = e.args[0][0][2]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if thirdarg == 'http request':
|
||||
# The client is talking HTTP to an HTTPS server.
|
||||
raise errors.NoSSLError()
|
||||
|
||||
raise errors.FatalSSLAlert(*e.args)
|
||||
|
||||
if time.time() - start > self.ssl_timeout:
|
||||
raise socket.timeout('timed out')
|
||||
|
||||
def recv(self, size):
|
||||
"""Receive message of a size from the socket."""
|
||||
return self._safe_call(
|
||||
True,
|
||||
super(SSLFileobjectMixin, self).recv,
|
||||
size,
|
||||
)
|
||||
|
||||
def readline(self, size=-1):
|
||||
"""Receive message of a size from the socket.
|
||||
|
||||
Matches the following interface:
|
||||
https://docs.python.org/3/library/io.html#io.IOBase.readline
|
||||
"""
|
||||
return self._safe_call(
|
||||
True,
|
||||
super(SSLFileobjectMixin, self).readline,
|
||||
size,
|
||||
)
|
||||
|
||||
def sendall(self, *args, **kwargs):
|
||||
"""Send whole message to the socket."""
|
||||
return self._safe_call(
|
||||
False,
|
||||
super(SSLFileobjectMixin, self).sendall,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
"""Send some part of message to the socket."""
|
||||
return self._safe_call(
|
||||
False,
|
||||
super(SSLFileobjectMixin, self).send,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader):
|
||||
"""SSL file object attached to a socket object."""
|
||||
|
||||
|
||||
class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter):
|
||||
"""SSL file object attached to a socket object."""
|
||||
|
||||
|
||||
class SSLConnectionProxyMeta:
|
||||
"""Metaclass for generating a bunch of proxy methods."""
|
||||
|
||||
def __new__(mcl, name, bases, nmspc):
|
||||
"""Attach a list of proxy methods to a new class."""
|
||||
proxy_methods = (
|
||||
'get_context', 'pending', 'send', 'write', 'recv', 'read',
|
||||
'renegotiate', 'bind', 'listen', 'connect', 'accept',
|
||||
'setblocking', 'fileno', 'close', 'get_cipher_list',
|
||||
'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
|
||||
'makefile', 'get_app_data', 'set_app_data', 'state_string',
|
||||
'sock_shutdown', 'get_peer_certificate', 'want_read',
|
||||
'want_write', 'set_connect_state', 'set_accept_state',
|
||||
'connect_ex', 'sendall', 'settimeout', 'gettimeout',
|
||||
'shutdown',
|
||||
)
|
||||
proxy_methods_no_args = (
|
||||
'shutdown',
|
||||
)
|
||||
|
||||
proxy_props = (
|
||||
'family',
|
||||
)
|
||||
|
||||
def lock_decorator(method):
|
||||
"""Create a proxy method for a new class."""
|
||||
def proxy_wrapper(self, *args):
|
||||
self._lock.acquire()
|
||||
try:
|
||||
new_args = (
|
||||
args[:] if method not in proxy_methods_no_args else []
|
||||
)
|
||||
return getattr(self._ssl_conn, method)(*new_args)
|
||||
finally:
|
||||
self._lock.release()
|
||||
return proxy_wrapper
|
||||
for m in proxy_methods:
|
||||
nmspc[m] = lock_decorator(m)
|
||||
nmspc[m].__name__ = m
|
||||
|
||||
def make_property(property_):
|
||||
"""Create a proxy method for a new class."""
|
||||
def proxy_prop_wrapper(self):
|
||||
return getattr(self._ssl_conn, property_)
|
||||
proxy_prop_wrapper.__name__ = property_
|
||||
return property(proxy_prop_wrapper)
|
||||
for p in proxy_props:
|
||||
nmspc[p] = make_property(p)
|
||||
|
||||
# Doesn't work via super() for some reason.
|
||||
# Falling back to type() instead:
|
||||
return type(name, bases, nmspc)
|
||||
|
||||
|
||||
class SSLConnection(metaclass=SSLConnectionProxyMeta):
|
||||
r"""A thread-safe wrapper for an ``SSL.Connection``.
|
||||
|
||||
:param tuple args: the arguments to create the wrapped \
|
||||
:py:class:`SSL.Connection(*args) \
|
||||
<pyopenssl:OpenSSL.SSL.Connection>`
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialize SSLConnection instance."""
|
||||
self._ssl_conn = SSL.Connection(*args)
|
||||
self._lock = threading.RLock()
|
||||
|
||||
|
||||
class pyOpenSSLAdapter(Adapter):
|
||||
"""A wrapper for integrating pyOpenSSL with Cheroot."""
|
||||
|
||||
certificate = None
|
||||
"""The file name of the server's TLS certificate."""
|
||||
|
||||
private_key = None
|
||||
"""The file name of the server's private key file."""
|
||||
|
||||
certificate_chain = None
|
||||
"""Optional. The file name of CA's intermediate certificate bundle.
|
||||
|
||||
This is needed for cheaper "chained root" TLS certificates,
|
||||
and should be left as :py:data:`None` if not required."""
|
||||
|
||||
context = None
|
||||
"""
|
||||
An instance of :py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`.
|
||||
"""
|
||||
|
||||
ciphers = None
|
||||
"""The ciphers list of TLS."""
|
||||
|
||||
def __init__(
|
||||
self, certificate, private_key, certificate_chain=None,
|
||||
ciphers=None,
|
||||
):
|
||||
"""Initialize OpenSSL Adapter instance."""
|
||||
if SSL is None:
|
||||
raise ImportError('You must install pyOpenSSL to use HTTPS.')
|
||||
|
||||
super(pyOpenSSLAdapter, self).__init__(
|
||||
certificate, private_key, certificate_chain, ciphers,
|
||||
)
|
||||
|
||||
self._environ = None
|
||||
|
||||
def bind(self, sock):
|
||||
"""Wrap and return the given socket."""
|
||||
if self.context is None:
|
||||
self.context = self.get_context()
|
||||
conn = SSLConnection(self.context, sock)
|
||||
self._environ = self.get_environ()
|
||||
return conn
|
||||
|
||||
def wrap(self, sock):
|
||||
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||||
# pyOpenSSL doesn't perform the handshake until the first read/write
|
||||
# forcing the handshake to complete tends to result in the connection
|
||||
# closing so we can't reliably access protocol/client cert for the env
|
||||
return sock, self._environ.copy()
|
||||
|
||||
def get_context(self):
|
||||
"""Return an ``SSL.Context`` from self attributes.
|
||||
|
||||
Ref: :py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`
|
||||
"""
|
||||
# See https://code.activestate.com/recipes/442473/
|
||||
c = SSL.Context(SSL.SSLv23_METHOD)
|
||||
c.use_privatekey_file(self.private_key)
|
||||
if self.certificate_chain:
|
||||
c.load_verify_locations(self.certificate_chain)
|
||||
c.use_certificate_file(self.certificate)
|
||||
return c
|
||||
|
||||
def get_environ(self):
|
||||
"""Return WSGI environ entries to be merged into each request."""
|
||||
ssl_environ = {
|
||||
'wsgi.url_scheme': 'https',
|
||||
'HTTPS': 'on',
|
||||
'SSL_VERSION_INTERFACE': '%s %s/%s Python/%s' % (
|
||||
cheroot_server.HTTPServer.version,
|
||||
OpenSSL.version.__title__, OpenSSL.version.__version__,
|
||||
sys.version,
|
||||
),
|
||||
'SSL_VERSION_LIBRARY': SSL.SSLeay_version(
|
||||
SSL.SSLEAY_VERSION,
|
||||
).decode(),
|
||||
}
|
||||
|
||||
if self.certificate:
|
||||
# Server certificate attributes
|
||||
with open(self.certificate, 'rb') as cert_file:
|
||||
cert = crypto.load_certificate(
|
||||
crypto.FILETYPE_PEM, cert_file.read(),
|
||||
)
|
||||
|
||||
ssl_environ.update({
|
||||
'SSL_SERVER_M_VERSION': cert.get_version(),
|
||||
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
|
||||
# 'SSL_SERVER_V_START':
|
||||
# Validity of server's certificate (start time),
|
||||
# 'SSL_SERVER_V_END':
|
||||
# Validity of server's certificate (end time),
|
||||
})
|
||||
|
||||
for prefix, dn in [
|
||||
('I', cert.get_issuer()),
|
||||
('S', cert.get_subject()),
|
||||
]:
|
||||
# X509Name objects don't seem to have a way to get the
|
||||
# complete DN string. Use str() and slice it instead,
|
||||
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
|
||||
dnstr = str(dn)[18:-2]
|
||||
|
||||
wsgikey = 'SSL_SERVER_%s_DN' % prefix
|
||||
ssl_environ[wsgikey] = dnstr
|
||||
|
||||
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
|
||||
# for any value to contain slashes itself (in a URL).
|
||||
while dnstr:
|
||||
pos = dnstr.rfind('=')
|
||||
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
|
||||
pos = dnstr.rfind('/')
|
||||
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
|
||||
if key and value:
|
||||
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
|
||||
ssl_environ[wsgikey] = value
|
||||
|
||||
return ssl_environ
|
||||
|
||||
def makefile(self, sock, mode='r', bufsize=-1):
|
||||
"""Return socket file object."""
|
||||
cls = (
|
||||
SSLFileobjectStreamReader
|
||||
if 'r' in mode else
|
||||
SSLFileobjectStreamWriter
|
||||
)
|
||||
if SSL and isinstance(sock, ssl_conn_type):
|
||||
wrapped_socket = cls(sock, mode, bufsize)
|
||||
wrapped_socket.ssl_timeout = sock.gettimeout()
|
||||
return wrapped_socket
|
||||
# This is from past:
|
||||
# TODO: figure out what it's meant for
|
||||
else:
|
||||
return cheroot_server.CP_fileobject(sock, mode, bufsize)
|
||||
@ -0,0 +1,31 @@
|
||||
from . import Adapter
|
||||
from ..makefile import StreamReader, StreamWriter
|
||||
from OpenSSL import SSL
|
||||
from typing import Any, Type
|
||||
|
||||
ssl_conn_type: Type[SSL.Connection]
|
||||
|
||||
class SSLFileobjectMixin:
|
||||
ssl_timeout: int
|
||||
ssl_retry: float
|
||||
def recv(self, size): ...
|
||||
def readline(self, size: int = ...): ...
|
||||
def sendall(self, *args, **kwargs): ...
|
||||
def send(self, *args, **kwargs): ...
|
||||
|
||||
class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ... # type:ignore[misc]
|
||||
class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ... # type:ignore[misc]
|
||||
|
||||
class SSLConnectionProxyMeta:
|
||||
def __new__(mcl, name, bases, nmspc): ...
|
||||
|
||||
class SSLConnection:
|
||||
def __init__(self, *args) -> None: ...
|
||||
|
||||
class pyOpenSSLAdapter(Adapter):
|
||||
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
|
||||
def bind(self, sock): ...
|
||||
def wrap(self, sock): ...
|
||||
def get_environ(self): ...
|
||||
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
|
||||
def get_context(self) -> SSL.Context: ...
|
||||
@ -0,0 +1 @@
|
||||
"""Cheroot test suite."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,43 @@
|
||||
"""Local pytest plugin.
|
||||
|
||||
Contains hooks, which are tightly bound to the Cheroot framework
|
||||
itself, useless for end-users' app testing.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
pytest_version = tuple(map(int, pytest.__version__.split('.')))
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
"""Drop unfilterable warning ignores."""
|
||||
if pytest_version < (6, 2, 0):
|
||||
return
|
||||
|
||||
# pytest>=6.2.0 under Python 3.8:
|
||||
# Refs:
|
||||
# * https://docs.pytest.org/en/stable/usage.html#unraisable
|
||||
# * https://github.com/pytest-dev/pytest/issues/5299
|
||||
early_config._inicache['filterwarnings'].extend((
|
||||
'ignore:Exception in thread CP Server Thread-:'
|
||||
'pytest.PytestUnhandledThreadExceptionWarning:_pytest.threadexception',
|
||||
'ignore:Exception in thread Thread-:'
|
||||
'pytest.PytestUnhandledThreadExceptionWarning:_pytest.threadexception',
|
||||
'ignore:Exception ignored in. '
|
||||
'<socket.socket fd=-1, family=AddressFamily.AF_INET, '
|
||||
'type=SocketKind.SOCK_STREAM, proto=.:'
|
||||
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
|
||||
'ignore:Exception ignored in. '
|
||||
'<socket.socket fd=-1, family=AddressFamily.AF_INET6, '
|
||||
'type=SocketKind.SOCK_STREAM, proto=.:'
|
||||
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
|
||||
'ignore:Exception ignored in. '
|
||||
'<socket.socket fd=-1, family=AF_INET, '
|
||||
'type=SocketKind.SOCK_STREAM, proto=.:'
|
||||
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
|
||||
'ignore:Exception ignored in. '
|
||||
'<socket.socket fd=-1, family=AF_INET6, '
|
||||
'type=SocketKind.SOCK_STREAM, proto=.:'
|
||||
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
|
||||
))
|
||||
@ -0,0 +1,83 @@
|
||||
"""Pytest configuration module.
|
||||
|
||||
Contains fixtures, which are tightly bound to the Cheroot framework
|
||||
itself, useless for end-users' app testing.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from .._compat import IS_MACOS, IS_WINDOWS # noqa: WPS436
|
||||
from ..server import Gateway, HTTPServer
|
||||
from ..testing import ( # noqa: F401 # pylint: disable=unused-import
|
||||
native_server, wsgi_server,
|
||||
)
|
||||
from ..testing import get_server_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_request_timeout():
|
||||
"""Return a common HTTP request timeout for tests with queries."""
|
||||
computed_timeout = 0.1
|
||||
|
||||
if IS_MACOS:
|
||||
computed_timeout *= 2
|
||||
|
||||
if IS_WINDOWS:
|
||||
computed_timeout *= 10
|
||||
|
||||
return computed_timeout
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
# pylint: disable=redefined-outer-name
|
||||
def wsgi_server_client(wsgi_server): # noqa: F811
|
||||
"""Create a test client out of given WSGI server."""
|
||||
return get_server_client(wsgi_server)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
# pylint: disable=redefined-outer-name
|
||||
def native_server_client(native_server): # noqa: F811
|
||||
"""Create a test client out of given HTTP server."""
|
||||
return get_server_client(native_server)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_server():
|
||||
"""Provision a server creator as a fixture."""
|
||||
def start_srv():
|
||||
bind_addr = yield
|
||||
if bind_addr is None:
|
||||
return
|
||||
httpserver = make_http_server(bind_addr)
|
||||
yield httpserver
|
||||
yield httpserver
|
||||
|
||||
srv_creator = iter(start_srv())
|
||||
next(srv_creator) # pylint: disable=stop-iteration-return
|
||||
yield srv_creator
|
||||
try:
|
||||
while True:
|
||||
httpserver = next(srv_creator)
|
||||
if httpserver is not None:
|
||||
httpserver.stop()
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
|
||||
def make_http_server(bind_addr):
|
||||
"""Create and start an HTTP server bound to ``bind_addr``."""
|
||||
httpserver = HTTPServer(
|
||||
bind_addr=bind_addr,
|
||||
gateway=Gateway,
|
||||
)
|
||||
|
||||
threading.Thread(target=httpserver.safe_start).start()
|
||||
|
||||
while not httpserver.ready:
|
||||
time.sleep(0.1)
|
||||
|
||||
return httpserver
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user