diff --git a/awesome_venv/bin/calc-prorate b/awesome_venv/bin/calc-prorate new file mode 100755 index 000000000..78078357f --- /dev/null +++ b/awesome_venv/bin/calc-prorate @@ -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()) diff --git a/awesome_venv/bin/cheroot b/awesome_venv/bin/cheroot new file mode 100755 index 000000000..09864499f --- /dev/null +++ b/awesome_venv/bin/cheroot @@ -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()) diff --git a/awesome_venv/bin/cherryd b/awesome_venv/bin/cherryd new file mode 100755 index 000000000..2d37152ab --- /dev/null +++ b/awesome_venv/bin/cherryd @@ -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()) diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/INSTALLER b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/LICENSE.md b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/LICENSE.md new file mode 100644 index 000000000..ce28cf8f2 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/LICENSE.md @@ -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. diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/METADATA b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/METADATA new file mode 100644 index 000000000..fa9836cdf --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/METADATA @@ -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 `_! + +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 `_ 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 +`_. + +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 `_. + +Contributing +------------ + +Please follow the `contribution guidelines +`_. +And by all means, absorb the `Zen of +CherryPy `_. diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/RECORD b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/RECORD new file mode 100644 index 000000000..7e37b8cfc --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/RECORD @@ -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 diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/REQUESTED b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/WHEEL b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/entry_points.txt b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/entry_points.txt new file mode 100644 index 000000000..60ca207b2 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +cherryd = cherrypy.__main__:run diff --git a/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/top_level.txt b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/top_level.txt new file mode 100644 index 000000000..d71870692 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/CherryPy-18.9.0.dist-info/top_level.txt @@ -0,0 +1 @@ +cherrypy diff --git a/awesome_venv/lib/python3.10/site-packages/__pycache__/portend.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/__pycache__/portend.cpython-310.pyc new file mode 100644 index 000000000..771b19d1f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/__pycache__/portend.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/INSTALLER b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/METADATA b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/METADATA new file mode 100644 index 000000000..36ec9b0d6 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/METADATA @@ -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 , Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Zac Hatfield-Dodds +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 + +[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types) +[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types) +[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](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! diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/RECORD b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/RECORD new file mode 100644 index 000000000..983c17359 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/RECORD @@ -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 diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/WHEEL b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/WHEEL new file mode 100644 index 000000000..ba1a8af28 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.18.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/licenses/LICENSE b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/licenses/LICENSE new file mode 100644 index 000000000..d99323a99 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types-0.6.0.dist-info/licenses/LICENSE @@ -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. diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types/__init__.py b/awesome_venv/lib/python3.10/site-packages/annotated_types/__init__.py new file mode 100644 index 000000000..2f989504c --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types/__init__.py @@ -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) diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..e8bb779d5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/test_cases.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/test_cases.cpython-310.pyc new file mode 100644 index 000000000..6adf27f1a Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/annotated_types/__pycache__/test_cases.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types/py.typed b/awesome_venv/lib/python3.10/site-packages/annotated_types/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/awesome_venv/lib/python3.10/site-packages/annotated_types/test_cases.py b/awesome_venv/lib/python3.10/site-packages/annotated_types/test_cases.py new file mode 100644 index 000000000..f54df7002 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/annotated_types/test_cases.py @@ -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]) diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/INSTALLER b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/LICENSE b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/LICENSE new file mode 100644 index 000000000..b49c3af06 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/LICENSE @@ -0,0 +1,166 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/METADATA b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/METADATA new file mode 100644 index 000000000..32214fb44 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/METADATA @@ -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 + +[![PyPI version](https://badge.fury.io/py/autocommand.svg)](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 `_ and `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. diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/RECORD b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/RECORD new file mode 100644 index 000000000..437aab7d7 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/RECORD @@ -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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/WHEEL b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/WHEEL new file mode 100644 index 000000000..57e3d840d --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.38.4) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/top_level.txt b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/top_level.txt new file mode 100644 index 000000000..dda5158ff --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand-2.2.2.dist-info/top_level.txt @@ -0,0 +1 @@ +autocommand diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__init__.py b/awesome_venv/lib/python3.10/site-packages/autocommand/__init__.py new file mode 100644 index 000000000..73fbfca6b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/__init__.py @@ -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 . + +# 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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..c3b7f792f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoasync.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoasync.cpython-310.pyc new file mode 100644 index 000000000..d0691be90 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoasync.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autocommand.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autocommand.cpython-310.pyc new file mode 100644 index 000000000..cbf1e315e Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autocommand.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/automain.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/automain.cpython-310.pyc new file mode 100644 index 000000000..2a2c21627 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/automain.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoparse.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoparse.cpython-310.pyc new file mode 100644 index 000000000..ae4c9c255 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/autoparse.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/errors.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/errors.cpython-310.pyc new file mode 100644 index 000000000..d21e6df79 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/autocommand/__pycache__/errors.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/autoasync.py b/awesome_venv/lib/python3.10/site-packages/autocommand/autoasync.py new file mode 100644 index 000000000..688f7e055 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/autoasync.py @@ -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 . + +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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/autocommand.py b/awesome_venv/lib/python3.10/site-packages/autocommand/autocommand.py new file mode 100644 index 000000000..097e86de0 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/autocommand.py @@ -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 . + +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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/automain.py b/awesome_venv/lib/python3.10/site-packages/autocommand/automain.py new file mode 100644 index 000000000..6cc45db66 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/automain.py @@ -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 . + +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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/autoparse.py b/awesome_venv/lib/python3.10/site-packages/autocommand/autoparse.py new file mode 100644 index 000000000..0276a3fae --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/autoparse.py @@ -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 . + +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 diff --git a/awesome_venv/lib/python3.10/site-packages/autocommand/errors.py b/awesome_venv/lib/python3.10/site-packages/autocommand/errors.py new file mode 100644 index 000000000..257060739 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/autocommand/errors.py @@ -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 . + + +class AutocommandError(Exception): + '''Base class for autocommand exceptions''' + pass + +# Individual modules will define errors specific to that module. diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/INSTALLER b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/LICENSE.md b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/LICENSE.md new file mode 100644 index 000000000..921640908 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/LICENSE.md @@ -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. diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/METADATA b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/METADATA new file mode 100644 index 000000000..03219c7d7 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/METADATA @@ -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: 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 +`_. +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 +`_ 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 + + diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/RECORD b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/RECORD new file mode 100644 index 000000000..45b5ed604 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/RECORD @@ -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 diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/WHEEL b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/WHEEL new file mode 100644 index 000000000..becc9a66e --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/entry_points.txt b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/entry_points.txt new file mode 100644 index 000000000..087790b4a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +cheroot = cheroot.cli:main + diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/top_level.txt b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/top_level.txt new file mode 100644 index 000000000..a610cf883 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot-10.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +cheroot diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.py b/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.py new file mode 100644 index 000000000..aac9cd987 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.py @@ -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' diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.pyi new file mode 100644 index 000000000..bda5b5a7f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/__init__.pyi @@ -0,0 +1 @@ +__version__: str diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__main__.py b/awesome_venv/lib/python3.10/site-packages/cheroot/__main__.py new file mode 100644 index 000000000..d2e27c108 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/__main__.py @@ -0,0 +1,6 @@ +"""Stub for accessing the Cheroot CLI tool.""" + +from .cli import main + +if __name__ == '__main__': + main() diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..daa2828c9 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__main__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 000000000..3a0b3ea0d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/__main__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/_compat.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/_compat.cpython-310.pyc new file mode 100644 index 000000000..5bf5d3be5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/_compat.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/cli.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/cli.cpython-310.pyc new file mode 100644 index 000000000..c6bbc66dc Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/cli.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/connections.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/connections.cpython-310.pyc new file mode 100644 index 000000000..7e5f75407 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/connections.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/errors.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/errors.cpython-310.pyc new file mode 100644 index 000000000..bbd6090f7 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/errors.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/makefile.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/makefile.cpython-310.pyc new file mode 100644 index 000000000..f4191cf3f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/makefile.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/server.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/server.cpython-310.pyc new file mode 100644 index 000000000..d267465df Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/server.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/testing.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/testing.cpython-310.pyc new file mode 100644 index 000000000..a723f9904 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/testing.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/wsgi.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/wsgi.cpython-310.pyc new file mode 100644 index 000000000..a3ea7947b Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/__pycache__/wsgi.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.py b/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.py new file mode 100644 index 000000000..dbe5c6d2f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.py @@ -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', + ) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.pyi new file mode 100644 index 000000000..67d93cf6c --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/_compat.pyi @@ -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: ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/cli.py b/awesome_venv/lib/python3.10/site-packages/cheroot/cli.py new file mode 100644 index 000000000..cd168e918 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/cli.py @@ -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 //@ as a valid url with a hostname + # with 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() diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/cli.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/cli.pyi new file mode 100644 index 000000000..b9803b3e0 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/cli.pyi @@ -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: ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/connections.py b/awesome_venv/lib/python3.10/site-packages/cheroot/connections.py new file mode 100644 index 000000000..9346bc6ae --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/connections.py @@ -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 ` 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 diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/connections.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/connections.pyi new file mode 100644 index 000000000..528ad7651 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/connections.pyi @@ -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): ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/errors.py b/awesome_venv/lib/python3.10/site-packages/cheroot/errors.py new file mode 100644 index 000000000..f6b588c2f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/errors.py @@ -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) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/errors.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/errors.pyi new file mode 100644 index 000000000..186695682 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/errors.pyi @@ -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], ...] diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.py b/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.py new file mode 100644 index 000000000..77878c13b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.py @@ -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) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.pyi new file mode 100644 index 000000000..3f5ea2756 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/makefile.pyi @@ -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=...): ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/py.typed b/awesome_venv/lib/python3.10/site-packages/cheroot/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/server.py b/awesome_venv/lib/python3.10/site-packages/cheroot/server.py new file mode 100644 index 000000000..bceeb2c95 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/server.py @@ -0,0 +1,2220 @@ +""" +A high-speed, production ready, thread pooled, generic HTTP server. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = HTTPServer(...) + server.start() + -> serve() + while ready: + _connections.run() + while not stop_requested: + child = socket.accept() # blocks until a request comes in + conn = HTTPConnection(child, ...) + server.process_conn(conn) # adds conn to threadpool + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return + +For running a server you can invoke :func:`start() ` (it +will run the server forever) or use invoking :func:`prepare() +` and :func:`serve() ` like this:: + + server = HTTPServer(...) + server.prepare() + try: + threading.Thread(target=server.serve).start() + + # waiting/detecting some appropriate stop condition here + ... + + finally: + server.stop() + +And now for a trivial doctest to exercise the test suite + +.. testsetup:: + + from cheroot.server import HTTPServer + +>>> 'HTTPServer' in globals() +True +""" + +import os +import io +import re +import email.utils +import socket +import sys +import time +import traceback as traceback_ +import logging +import platform +import queue +import contextlib +import threading +import urllib.parse +from functools import lru_cache + +from . import connections, errors, __version__ +from ._compat import bton +from ._compat import IS_PPC +from .workers import threadpool +from .makefile import MakeFile, StreamWriter + + +__all__ = ( + 'HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'HeaderReader', 'DropUnderscoreHeaderReader', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'Gateway', 'get_ssl_adapter_class', +) + + +IS_WINDOWS = platform.system() == 'Windows' +"""Flag indicating whether the app is running under Windows.""" + + +IS_GAE = os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/') +"""Flag indicating whether the app is running in GAE env. + +Ref: +https://cloud.google.com/appengine/docs/standard/python/tools +/using-local-server#detecting_application_runtime_environment +""" + + +IS_UID_GID_RESOLVABLE = not IS_WINDOWS and not IS_GAE +"""Indicates whether UID/GID resolution's available under current platform.""" + + +if IS_UID_GID_RESOLVABLE: + try: + import grp + import pwd + except ImportError: + """Unavailable in the current env. + + This shouldn't be happening normally. + All of the known cases are excluded via the if clause. + """ + IS_UID_GID_RESOLVABLE = False + grp, pwd = None, None + import struct + + +if IS_WINDOWS and hasattr(socket, 'AF_INET6'): + if not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 + if not hasattr(socket, 'IPV6_V6ONLY'): + socket.IPV6_V6ONLY = 27 + + +if not hasattr(socket, 'SO_PEERCRED'): + """ + NOTE: the value for SO_PEERCRED can be architecture specific, in + which case the getsockopt() will hopefully fail. The arch + specific value could be derived from platform.processor() + """ + socket.SO_PEERCRED = 21 if IS_PPC else 17 + + +LF = b'\n' +CRLF = b'\r\n' +TAB = b'\t' +SPACE = b' ' +COLON = b':' +SEMICOLON = b';' +EMPTY = b'' +ASTERISK = b'*' +FORWARD_SLASH = b'/' +QUOTED_SLASH = b'%2F' +QUOTED_SLASH_REGEX = re.compile(b''.join((b'(?i)', QUOTED_SLASH))) + + +_STOPPING_FOR_INTERRUPT = object() # sentinel used during shutdown + + +comma_separated_headers = [ + b'Accept', b'Accept-Charset', b'Accept-Encoding', + b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control', + b'Connection', b'Content-Encoding', b'Content-Language', b'Expect', + b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE', + b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning', + b'WWW-Authenticate', +] + + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +class HeaderReader: + """Object for reading headers from an HTTP request. + + Interface and default implementation. + """ + + def __call__(self, rfile, hdict=None): # noqa: C901 # FIXME + """ + Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP + spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError('Illegal header line.') + v = v.strip() + k = self._transform_key(k) + hname = k + + if not self._allow_header(k): + continue + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = b', '.join((existing, v)) + hdict[hname] = v + + return hdict + + def _allow_header(self, key_name): + return True + + def _transform_key(self, key_name): + # TODO: what about TE and WWW-Authenticate? + return key_name.strip().title() + + +class DropUnderscoreHeaderReader(HeaderReader): + """Custom HeaderReader to exclude any headers with underscores in them.""" + + def _allow_header(self, key_name): + orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name) + return orig and '_' not in key_name + + +class SizeCheckWrapper: + """Wraps a file-like object, raising MaxSizeExceeded if too large. + + :param rfile: ``file`` of a limited size + :param int maxlen: maximum length of the file being read + """ + + def __init__(self, rfile, maxlen): + """Initialize SizeCheckWrapper instance.""" + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded() + + def read(self, size=None): + """Read a chunk from ``rfile`` buffer and return it. + + :param size: amount of data to read + :type size: int + + :returns: chunk from ``rfile``, limited by size if specified + :rtype: bytes + """ + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + """Read a single line from ``rfile`` buffer and return it. + + :param size: minimum amount of data to read + :type size: int + + :returns: one line from ``rfile`` + :rtype: bytes + """ + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See https://github.com/cherrypy/cherrypy/issues/421 + if len(data) < 256 or data[-1:] == LF: + return EMPTY.join(res) + + def readlines(self, sizehint=0): + """Read all lines from ``rfile`` buffer and return them. + + :param sizehint: hint of minimum amount of data to read + :type sizehint: int + + :returns: lines of bytes read from ``rfile`` + :rtype: list[bytes] + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for ``rfile``.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + next = __next__ + + +class KnownLengthRFile: + """Wraps a file-like object, returning an empty string when exhausted. + + :param rfile: ``file`` of a known size + :param int content_length: length of the file being read + """ + + def __init__(self, rfile, content_length): + """Initialize KnownLengthRFile instance.""" + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + """Read a chunk from ``rfile`` buffer and return it. + + :param size: amount of data to read + :type size: int + + :rtype: bytes + :returns: chunk from ``rfile``, limited by size if specified + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + """Read a single line from ``rfile`` buffer and return it. + + :param size: minimum amount of data to read + :type size: int + + :returns: one line from ``rfile`` + :rtype: bytes + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + """Read all lines from ``rfile`` buffer and return them. + + :param sizehint: hint of minimum amount of data to read + :type sizehint: int + + :returns: lines of bytes read from ``rfile`` + :rtype: list[bytes] + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for ``rfile``.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.remaining -= len(data) + return data + + next = __next__ + + +class ChunkedRFile: + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + + :param rfile: file encoded with the 'chunked' transfer encoding + :param int maxlen: maximum length of the file being read + :param int bufsize: size of the buffer used to read the file + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + """Initialize ChunkedRFile instance.""" + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded( + 'Request Entity Too Large', self.maxlen, + ) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError( + 'Bad chunked transfer size: {chunk_size!r}'. + format(chunk_size=chunk_size), + ) + + if chunk_size <= 0: + self.closed = True + return + +# if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError('Request Entity Too Large') + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + 'got ' + repr(crlf) + ')', + ) + + def read(self, size=None): + """Read a chunk from ``rfile`` buffer and return it. + + :param size: amount of data to read + :type size: int + + :returns: chunk from ``rfile``, limited by size if specified + :rtype: bytes + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + self.buffer = EMPTY + + def readline(self, size=None): + """Read a single line from ``rfile`` buffer and return it. + + :param size: minimum amount of data to read + :type size: int + + :returns: one line from ``rfile`` + :rtype: bytes + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + self.buffer = EMPTY + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + """Read all lines from ``rfile`` buffer and return them. + + :param sizehint: hint of minimum amount of data to read + :type sizehint: int + + :returns: lines of bytes read from ``rfile`` + :rtype: list[bytes] + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + """Read HTTP headers and yield them. + + :yields: CRLF separated lines + :ytype: bytes + + """ + if not self.closed: + raise ValueError( + 'Cannot read trailers until the request body has been read.', + ) + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError('Request Entity Too Large') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + yield line + + def close(self): + """Release resources allocated for ``rfile``.""" + self.rfile.close() + + +class HTTPRequest: + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + header_reader = HeaderReader() + """ + A HeaderReader instance or compatible reader. + """ + + def __init__(self, server, conn, proxy_mode=False, strict_mode=True): + """Initialize HTTP request container instance. + + Args: + server (HTTPServer): web server object receiving this request + conn (HTTPConnection): HTTP connection object for this request + proxy_mode (bool): whether this HTTPServer should behave as a PROXY + server for certain requests + strict_mode (bool): whether we should return a 400 Bad Request when + we encounter a request that a HTTP compliant client should not be + making + """ + self.server = server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = b'http' + if self.server.ssl_adapter is not None: + self.scheme = b'https' + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = '' + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + self.proxy_mode = proxy_mode + self.strict_mode = strict_mode + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper( + self.conn.rfile, + self.server.max_request_header_size, + ) + try: + success = self.read_request_line() + except errors.MaxSizeExceeded: + self.simple_response( + '414 Request-URI Too Long', + 'The Request-URI sent with the request exceeds the maximum ' + 'allowed bytes.', + ) + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except errors.MaxSizeExceeded: + self.simple_response( + '413 Request Entity Too Large', + 'The headers sent with the request exceed the maximum ' + 'allowed bytes.', + ) + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): # noqa: C901 # FIXME + """Read and parse first line of the HTTP request. + + Returns: + bool: True if the request line is valid or False if it's malformed. + + """ + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response( + '400 Bad Request', 'HTTP requires CRLF terminators', + ) + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + if not req_protocol.startswith(b'HTTP/'): + self.simple_response( + '400 Bad Request', 'Malformed Request-Line: bad protocol', + ) + return False + rp = req_protocol[5:].split(b'.', 1) + if len(rp) != 2: + self.simple_response( + '400 Bad Request', 'Malformed Request-Line: bad version', + ) + return False + rp = tuple(map(int, rp)) # Minor.Major must be threat as integers + if rp > (1, 1): + self.simple_response( + '505 HTTP Version Not Supported', 'Cannot fulfill request', + ) + return False + except (ValueError, IndexError): + self.simple_response('400 Bad Request', 'Malformed Request-Line') + return False + + self.uri = uri + self.method = method.upper() + + if self.strict_mode and method != self.method: + resp = ( + 'Malformed method name: According to RFC 2616 ' + '(section 5.1.1) and its successors ' + 'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) ' + 'method names are case-sensitive and uppercase.' + ) + self.simple_response('400 Bad Request', resp) + return False + + try: + scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) + except UnicodeError: + self.simple_response('400 Bad Request', 'Malformed Request-URI') + return False + + uri_is_absolute_form = (scheme or authority) + + if self.method == b'OPTIONS': + # TODO: cover this branch with tests + path = ( + uri + # https://tools.ietf.org/html/rfc7230#section-5.3.4 + if (self.proxy_mode and uri_is_absolute_form) + else path + ) + elif self.method == b'CONNECT': + # TODO: cover this branch with tests + if not self.proxy_mode: + self.simple_response('405 Method Not Allowed') + return False + + # `urlsplit()` above parses "example.com:3128" as path part of URI. + # this is a workaround, which makes it detect netloc correctly + uri_split = urllib.parse.urlsplit(b''.join((b'//', uri))) + _scheme, _authority, _path, _qs, _fragment = uri_split + _port = EMPTY + try: + _port = uri_split.port + except ValueError: + pass + + # FIXME: use third-party validation to make checks against RFC + # the validation doesn't take into account, that urllib parses + # invalid URIs without raising errors + # https://tools.ietf.org/html/rfc7230#section-5.3.3 + invalid_path = ( + _authority != uri + or not _port + or any((_scheme, _path, _qs, _fragment)) + ) + if invalid_path: + self.simple_response( + '400 Bad Request', + 'Invalid path in Request-URI: request-' + 'target must match authority-form.', + ) + return False + + authority = path = _authority + scheme = qs = fragment = EMPTY + else: + disallowed_absolute = ( + self.strict_mode + and not self.proxy_mode + and uri_is_absolute_form + ) + if disallowed_absolute: + # https://tools.ietf.org/html/rfc7230#section-5.3.2 + # (absolute form) + """Absolute URI is only allowed within proxies.""" + self.simple_response( + '400 Bad Request', + 'Absolute URI not allowed if server is not a proxy.', + ) + return False + + invalid_path = ( + self.strict_mode + and not uri.startswith(FORWARD_SLASH) + and not uri_is_absolute_form + ) + if invalid_path: + # https://tools.ietf.org/html/rfc7230#section-5.3.1 + # (origin_form) and + """Path should start with a forward slash.""" + resp = ( + 'Invalid path in Request-URI: request-target must contain ' + 'origin-form which starts with absolute-path (URI ' + 'starting with a slash "/").' + ) + self.simple_response('400 Bad Request', resp) + return False + + if fragment: + self.simple_response( + '400 Bad Request', + 'Illegal #fragment in Request-URI.', + ) + return False + + if path is None: + # FIXME: It looks like this case cannot happen + self.simple_response( + '400 Bad Request', + 'Invalid path in Request-URI.', + ) + return False + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not + # "/this/path". + try: + # TODO: Figure out whether exception can really happen here. + # It looks like it's caught on urlsplit() call above. + atoms = [ + urllib.parse.unquote_to_bytes(x) + for x in QUOTED_SLASH_REGEX.split(path) + ] + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + path = QUOTED_SLASH.join(atoms) + + if not path.startswith(FORWARD_SLASH): + path = FORWARD_SLASH + path + + if scheme is not EMPTY: + self.scheme = scheme + self.authority = authority + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response('505 HTTP Version Not Supported') + return False + + self.request_protocol = req_protocol + self.response_protocol = 'HTTP/%s.%s' % min(rp, sp) + + return True + + def read_request_headers(self): # noqa: C901 # FIXME + """Read ``self.rfile`` into ``self.inheaders``. + + Ref: :py:attr:`self.inheaders `. + + :returns: success status + :rtype: bool + """ + # then all the http headers + try: + self.header_reader(self.rfile, self.inheaders) + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + + try: + cl = int(self.inheaders.get(b'Content-Length', 0)) + except ValueError: + self.simple_response( + '400 Bad Request', + 'Malformed Content-Length Header.', + ) + return False + + if mrbs and cl > mrbs: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the maximum ' + 'allowed bytes.', + ) + return False + + # Persistent connection support + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 + if self.inheaders.get(b'Connection', b'') == b'close': + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get(b'Connection', b'') != b'Keep-Alive': + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == 'HTTP/1.1': + te = self.inheaders.get(b'Transfer-Encoding') + if te: + te = [x.strip().lower() for x in te.split(b',') if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == b'chunked': + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response('501 Unimplemented') + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get(b'Expect', b'') == b'100-continue': + # Don't use simple_response here, because it emits headers + # we don't want. See + # https://github.com/cherrypy/cherrypy/issues/951 + msg = b''.join(( + self.server.protocol.encode('ascii'), SPACE, b'100 Continue', + CRLF, CRLF, + )) + try: + self.conn.wfile.write(msg) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return True + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get(b'Content-Length', 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the ' + 'maximum allowed bytes.', + ) + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + self.ready and self.ensure_headers_sent() + + if self.chunked_write: + self.conn.wfile.write(b'0\r\n\r\n') + + def simple_response(self, status, msg=''): + """Write a simple response back to the client.""" + status = str(status) + proto_status = '%s %s\r\n' % (self.server.protocol, status) + content_length = 'Content-Length: %s\r\n' % len(msg) + content_type = 'Content-Type: text/plain\r\n' + buf = [ + proto_status.encode('ISO-8859-1'), + content_length.encode('ISO-8859-1'), + content_type.encode('ISO-8859-1'), + ] + + if status[:3] in ('413', '414'): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append(b'Connection: close\r\n') + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = '400 Bad Request' + + buf.append(CRLF) + if msg: + if isinstance(msg, str): + msg = msg.encode('ISO-8859-1') + buf.append(msg) + + try: + self.conn.wfile.write(EMPTY.join(buf)) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + + def ensure_headers_sent(self): + """Ensure headers are sent to the client if not already sent.""" + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + chunk_size_hex = hex(len(chunk))[2:].encode('ascii') + buf = [chunk_size_hex, CRLF, chunk, CRLF] + self.conn.wfile.write(EMPTY.join(buf)) + else: + self.conn.wfile.write(chunk) + + def send_headers(self): # noqa: C901 # FIXME + """Assert, process, and send the HTTP response message-headers. + + You must set ``self.status``, and :py:attr:`self.outheaders + ` before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif b'content-length' not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + needs_chunked = ( + self.response_protocol == 'HTTP/1.1' + and self.method != b'HEAD' + ) + if needs_chunked: + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append((b'Transfer-Encoding', b'chunked')) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + # Override the decision to not close the connection if the connection + # manager doesn't have space for it. + if not self.close_connection: + can_keep = self.server.can_add_keepalive_connection + self.close_connection = not can_keep + + if b'connection' not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append((b'Connection', b'close')) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append((b'Connection', b'Keep-Alive')) + + if (b'Connection', b'Keep-Alive') in self.outheaders: + self.outheaders.append(( + b'Keep-Alive', + u'timeout={connection_timeout}'. + format(connection_timeout=self.server.timeout). + encode('ISO-8859-1'), + )) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if b'date' not in hkeys: + self.outheaders.append(( + b'Date', + email.utils.formatdate(usegmt=True).encode('ISO-8859-1'), + )) + + if b'server' not in hkeys: + self.outheaders.append(( + b'Server', + self.server.server_name.encode('ISO-8859-1'), + )) + + proto = self.server.protocol.encode('ascii') + buf = [proto + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.write(EMPTY.join(buf)) + + +class HTTPConnection: + """An HTTP connection (active socket).""" + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = io.DEFAULT_BUFFER_SIZE + wbufsize = io.DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + peercreds_enabled = False + peercreds_resolve_enabled = False + + # Fields set by ConnectionManager. + last_used = None + + def __init__(self, server, sock, makefile=MakeFile): + """Initialize HTTPConnection instance. + + Args: + server (HTTPServer): web server object receiving this request + sock (socket._socketobject): the raw socket object (usually + TCP) for this connection + makefile (file): a fileobject class for reading from the socket + """ + self.server = server + self.socket = sock + self.rfile = makefile(sock, 'rb', self.rbufsize) + self.wfile = makefile(sock, 'wb', self.wbufsize) + self.requests_seen = 0 + + self.peercreds_enabled = self.server.peercreds_enabled + self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled + + # LRU cached methods: + # Ref: https://stackoverflow.com/a/14946506/595220 + self.resolve_peer_creds = ( + lru_cache(maxsize=1)(self.resolve_peer_creds) + ) + self.get_peer_creds = ( + lru_cache(maxsize=1)(self.get_peer_creds) + ) + + def communicate(self): # noqa: C901 # FIXME + """Read each request and respond appropriately. + + Returns true if the connection should be kept open. + """ + request_seen = False + try: + req = self.RequestHandlerClass(self.server, self) + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return False + + request_seen = True + req.respond() + if not req.close_connection: + return True + except socket.error as ex: + errnum = ex.args[0] + # sadly SSL sockets return a different (longer) time out string + timeout_errs = 'timed out', 'The read operation timed out' + if errnum in timeout_errs: + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See https://github.com/cherrypy/cherrypy/issues/853 + if (not request_seen) or (req and req.started_request): + self._conditional_error(req, '408 Request Timeout') + elif errnum not in errors.socket_errors_to_ignore: + self.server.error_log( + 'socket.error %s' % repr(errnum), + level=logging.WARNING, traceback=True, + ) + self._conditional_error(req, '500 Internal Server Error') + except (KeyboardInterrupt, SystemExit): + raise + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + except Exception as ex: + self.server.error_log( + repr(ex), level=logging.ERROR, traceback=True, + ) + self._conditional_error(req, '500 Internal Server Error') + return False + + linger = False + + def _handle_no_ssl(self, req): + if not req or req.sent_headers: + return + # Unwrap wfile + try: + resp_sock = self.socket._sock + except AttributeError: + # self.socket is of OpenSSL.SSL.Connection type + resp_sock = self.socket._socket + self.wfile = StreamWriter(resp_sock, 'wb', self.wbufsize) + msg = ( + 'The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + req.simple_response('400 Bad Request', msg) + self.linger = True + + def _conditional_error(self, req, response): + """Respond with an error. + + Don't bother writing if a response + has already started being written. + """ + if not req or req.sent_headers: + return + + try: + req.simple_response(response) + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + self._close_kernel_socket() + # close the socket file descriptor + # (will be closed in the OS if there is no + # other reference to the underlying socket) + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + def get_peer_creds(self): # LRU cached on per-instance basis, see __init__ + """Return the PID/UID/GID tuple of the peer socket for UNIX sockets. + + This function uses SO_PEERCRED to query the UNIX PID, UID, GID + of the peer, which is only available if the bind address is + a UNIX domain socket. + + Raises: + NotImplementedError: in case of unsupported socket type + RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled + + """ + PEERCRED_STRUCT_DEF = '3i' + + if IS_WINDOWS or self.socket.family != socket.AF_UNIX: + raise NotImplementedError( + 'SO_PEERCRED is only supported in Linux kernel and WSL', + ) + elif not self.peercreds_enabled: + raise RuntimeError( + 'Peer creds lookup is disabled within this server', + ) + + try: + peer_creds = self.socket.getsockopt( + # FIXME: Use LOCAL_CREDS for BSD-like OSs + # Ref: https://gist.github.com/LucaFilipozzi/e4f1e118202aff27af6aadebda1b5d91 # noqa + socket.SOL_SOCKET, socket.SO_PEERCRED, + struct.calcsize(PEERCRED_STRUCT_DEF), + ) + except socket.error as socket_err: + """Non-Linux kernels don't support SO_PEERCRED. + + Refs: + http://welz.org.za/notes/on-peer-cred.html + https://github.com/daveti/tcpSockHack + msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025 + """ + raise RuntimeError from socket_err + else: + pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds) + return pid, uid, gid + + @property + def peer_pid(self): + """Return the id of the connected peer process.""" + pid, _, _ = self.get_peer_creds() + return pid + + @property + def peer_uid(self): + """Return the user id of the connected peer process.""" + _, uid, _ = self.get_peer_creds() + return uid + + @property + def peer_gid(self): + """Return the group id of the connected peer process.""" + _, _, gid = self.get_peer_creds() + return gid + + def resolve_peer_creds(self): # LRU cached on per-instance basis + """Look up the username and group tuple of the ``PEERCREDS``. + + :returns: the username and group tuple of the ``PEERCREDS`` + + :raises NotImplementedError: if the OS is unsupported + :raises RuntimeError: if UID/GID lookup is unsupported or disabled + """ + if not IS_UID_GID_RESOLVABLE: + raise NotImplementedError( + 'UID/GID lookup is unavailable under current platform. ' + 'It can only be done under UNIX-like OS ' + 'but not under the Google App Engine', + ) + elif not self.peercreds_resolve_enabled: + raise RuntimeError( + 'UID/GID lookup is disabled within this server', + ) + + user = pwd.getpwuid(self.peer_uid).pw_name # [0] + group = grp.getgrgid(self.peer_gid).gr_name # [0] + + return user, group + + @property + def peer_user(self): + """Return the username of the connected peer process.""" + user, _ = self.resolve_peer_creds() + return user + + @property + def peer_group(self): + """Return the group of the connected peer process.""" + _, group = self.resolve_peer_creds() + return group + + def _close_kernel_socket(self): + """Terminate the connection at the transport level.""" + # Honor ``sock_shutdown`` for PyOpenSSL connections. + shutdown = getattr( + self.socket, 'sock_shutdown', + self.socket.shutdown, + ) + + try: + shutdown(socket.SHUT_RDWR) # actually send a TCP FIN + except errors.acceptable_sock_shutdown_exceptions: + pass + except socket.error as e: + if e.errno not in errors.acceptable_sock_shutdown_error_codes: + raise + + +class HTTPServer: + """An HTTP server.""" + + _bind_addr = '127.0.0.1' + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create. + + (default -1 = no limit)""" + + server_name = None + """The name of the server; defaults to ``self.version``.""" + + protocol = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections. + + (default 5).""" + + shutdown_timeout = 5 + """The total time to wait for worker threads to cleanly exit. + + Specified in seconds.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + expiration_interval = 0.5 + """The interval, in seconds, at which the server checks for + expired connections (default 0.5). + """ + + version = 'Cheroot/{version!s}'.format(version=__version__) + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``. + """ + + ready = False + """Internal flag which indicating the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of ``ssl.Adapter`` (or a subclass). + + Ref: :py:class:`ssl.Adapter `. + + You must have the corresponding TLS driver library installed. + """ + + peercreds_enabled = False + """ + If :py:data:`True`, peer creds will be looked up via UNIX domain socket. + """ + + peercreds_resolve_enabled = False + """ + If :py:data:`True`, username/group will be looked up in the OS from + ``PEERCREDS``-provided IDs. + """ + + reuse_port = False + """If True, set SO_REUSEPORT on the socket.""" + + keep_alive_conn_limit = 10 + """Maximum number of waiting keep-alive connections that will be kept open. + + Default is 10. Set to None to have unlimited connections.""" + + def __init__( + self, bind_addr, gateway, + minthreads=10, maxthreads=-1, server_name=None, + peercreds_enabled=False, peercreds_resolve_enabled=False, + reuse_port=False, + ): + """Initialize HTTPServer instance. + + Args: + bind_addr (tuple): network interface to listen to + gateway (Gateway): gateway for processing HTTP requests + minthreads (int): minimum number of threads for HTTP thread pool + maxthreads (int): maximum number of threads for HTTP thread pool + server_name (str): web server name to be advertised via Server + HTTP header + reuse_port (bool): if True SO_REUSEPORT option would be set to + socket + """ + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = threadpool.ThreadPool( + self, min=minthreads or 1, max=maxthreads, + ) + + if not server_name: + server_name = self.version + self.server_name = server_name + self.peercreds_enabled = peercreds_enabled + self.peercreds_resolve_enabled = ( + peercreds_resolve_enabled and peercreds_enabled + ) + self.reuse_port = reuse_port + self.clear_stats() + + def clear_stats(self): + """Reset server stat counters..""" + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, 'qsize', None), + 'Threads': lambda s: len(getattr(self.requests, '_threads', [])), + 'Threads Idle': lambda s: getattr(self.requests, 'idle', None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum( + (w['Requests'](w) for w in s['Worker Threads'].values()), 0, + ), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum( + (w['Bytes Read'](w) for w in s['Worker Threads'].values()), 0, + ), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum( + (w['Bytes Written'](w) for w in s['Worker Threads'].values()), + 0, + ), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum( + (w['Work Time'](w) for w in s['Worker Threads'].values()), 0, + ), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + ( + w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values() + ), 0, + ), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + ( + w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values() + ), 0, + ), + 'Worker Threads': {}, + } + logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats + + def runtime(self): + """Return server uptime.""" + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + """Render Server instance representing bind address.""" + return '%s.%s(%r)' % ( + self.__module__, self.__class__.__name__, + self.bind_addr, + ) + + @property + def bind_addr(self): + """Return the interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any + :term:`IPv4` or :term:`IPv6` address, or any valid hostname. + The string 'localhost' is a synonym for '127.0.0.1' (or '::1', + if your hosts file prefers :term:`IPv6`). + The string '0.0.0.0' is a special :term:`IPv4` entry meaning + "any active interface" (INADDR_ANY), and '::' is the similar + IN6ADDR_ANY for :term:`IPv6`. + The empty string or :py:data:`None` are not allowed. + + For UNIX sockets, supply the file name as a string. + + Systemd socket activation is automatic and doesn't require tempering + with this variable. + + .. glossary:: + + :abbr:`IPv4 (Internet Protocol version 4)` + Internet Protocol version 4 + + :abbr:`IPv6 (Internet Protocol version 6)` + Internet Protocol version 6 + """ + return self._bind_addr + + @bind_addr.setter + def bind_addr(self, value): + """Set the interface on which to listen for connections.""" + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError( + "Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + 'to listen on all active interfaces.', + ) + self._bind_addr = value + + def safe_start(self): + """Run the server forever, and stop it cleanly on exit.""" + try: + self.start() + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.error_log('Keyboard Interrupt: shutting down') + self.stop() + raise + except SystemExit: + self.error_log('SystemExit raised: shutting down') + self.stop() + raise + + def prepare(self): # noqa: C901 # FIXME + """Prepare server to serving requests. + + It binds a socket's port, setups the socket to ``listen()`` and does + other preparing things. + """ + self._interrupt = None + + if self.software is None: + self.software = '%s Server' % self.version + + # Select the appropriate socket + self.socket = None + msg = 'No socket could be created' + if os.getenv('LISTEN_PID', None): + # systemd socket activation + self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) + elif isinstance(self.bind_addr, (str, bytes)): + # AF_UNIX socket + try: + self.bind_unix_socket(self.bind_addr) + except socket.error as serr: + msg = '%s -- (%s: %s)' % (msg, self.bind_addr, serr) + raise socket.error(msg) from serr + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 + # addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo( + host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE, + ) + except socket.gaierror: + sock_type = socket.AF_INET + bind_addr = self.bind_addr + + if ':' in host: + sock_type = socket.AF_INET6 + bind_addr = bind_addr + (0, 0) + + info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)] + + for res in info: + af, socktype, proto, _canonname, sa = res + try: + self.bind(af, socktype, proto) + break + except socket.error as serr: + msg = '%s -- (%s: %s)' % (msg, sa, serr) + if self.socket: + self.socket.close() + self.socket = None + + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # must not be accessed once stop() has been called + self._connections = connections.ConnectionManager(self) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + + def serve(self): + """Serve requests, after invoking :func:`prepare()`.""" + while self.ready and not self.interrupt: + try: + self._connections.run(self.expiration_interval) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.error_log( + 'Error in HTTPServer.serve', level=logging.ERROR, + traceback=True, + ) + + # raise exceptions reported by any worker threads, + # such that the exception is raised from the serve() thread. + if self.interrupt: + while self._stopping_for_interrupt: + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def start(self): + """Run the server forever. + + It is shortcut for invoking :func:`prepare()` then :func:`serve()`. + """ + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrypy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self.prepare() + self.serve() + + @contextlib.contextmanager + def _run_in_thread(self): + """Context manager for running this server in a thread.""" + self.prepare() + thread = threading.Thread(target=self.serve) + thread.daemon = True + thread.start() + try: + yield thread + finally: + self.stop() + + @property + def can_add_keepalive_connection(self): + """Flag whether it is allowed to add a new keep-alive connection.""" + return self.ready and self._connections.can_add_keepalive_connection + + def put_conn(self, conn): + """Put an idle connection back into the ConnectionManager.""" + if self.ready: + self._connections.put(conn) + else: + # server is shutting down, just close it + conn.close() + + def error_log(self, msg='', level=20, traceback=False): + """Write error message to log. + + Args: + msg (str): error message + level (int): logging level + traceback (bool): add traceback to output or not + """ + # Override this in subclasses as desired + sys.stderr.write('{msg!s}\n'.format(msg=msg)) + sys.stderr.flush() + if traceback: + tblines = traceback_.format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + sock = self.prepare_socket( + self.bind_addr, + family, type, proto, + self.nodelay, self.ssl_adapter, + self.reuse_port, + ) + sock = self.socket = self.bind_socket(sock, self.bind_addr) + self.bind_addr = self.resolve_real_bind_addr(sock) + return sock + + def bind_unix_socket(self, bind_addr): # noqa: C901 # FIXME + """Create (or recreate) a UNIX socket object.""" + if IS_WINDOWS: + """ + Trying to access socket.AF_UNIX under Windows + causes an AttributeError. + """ + raise ValueError( # or RuntimeError? + 'AF_UNIX sockets are not supported under Windows.', + ) + + fs_permissions = 0o777 # TODO: allow changing mode + + try: + # Make possible reusing the socket... + os.unlink(self.bind_addr) + except OSError: + """ + File does not exist, which is the primary goal anyway. + """ + except TypeError as typ_err: + err_msg = str(typ_err) + if ( + 'remove() argument 1 must be encoded ' + 'string without null bytes, not unicode' + not in err_msg + ): + raise + except ValueError as val_err: + err_msg = str(val_err) + if ( + 'unlink: embedded null ' + 'character in path' not in err_msg + and 'embedded null byte' not in err_msg + and 'argument must be a ' + 'string without NUL characters' not in err_msg # pypy3 + ): + raise + + sock = self.prepare_socket( + bind_addr=bind_addr, + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0, + nodelay=self.nodelay, ssl_adapter=self.ssl_adapter, + reuse_port=self.reuse_port, + ) + + try: + """Linux way of pre-populating fs mode permissions.""" + # Allow everyone access the socket... + os.fchmod(sock.fileno(), fs_permissions) + FS_PERMS_SET = True + except OSError: + FS_PERMS_SET = False + + try: + sock = self.bind_socket(sock, bind_addr) + except socket.error: + sock.close() + raise + + bind_addr = self.resolve_real_bind_addr(sock) + + try: + """FreeBSD/macOS pre-populating fs mode permissions.""" + if not FS_PERMS_SET: + try: + os.lchmod(bind_addr, fs_permissions) + except AttributeError: + os.chmod(bind_addr, fs_permissions, follow_symlinks=False) + FS_PERMS_SET = True + except OSError: + pass + + if not FS_PERMS_SET: + self.error_log( + 'Failed to set socket fs mode permissions', + level=logging.WARNING, + ) + + self.bind_addr = bind_addr + self.socket = sock + return sock + + @staticmethod + def _make_socket_reusable(socket_, bind_addr): + host, port = bind_addr[:2] + IS_EPHEMERAL_PORT = port == 0 + + if socket_.family not in (socket.AF_INET, socket.AF_INET6): + raise ValueError('Cannot reuse a non-IP socket') + + if IS_EPHEMERAL_PORT: + raise ValueError('Cannot reuse an ephemeral port (0)') + + # Most BSD kernels implement SO_REUSEPORT the way that only the + # latest listener can read from socket. Some of BSD kernels also + # have SO_REUSEPORT_LB that works similarly to SO_REUSEPORT + # in Linux. + if hasattr(socket, 'SO_REUSEPORT_LB'): + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT_LB, 1) + elif hasattr(socket, 'SO_REUSEPORT'): + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + elif IS_WINDOWS: + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + raise NotImplementedError( + 'Current platform does not support port reuse', + ) + + @classmethod + def prepare_socket( + cls, bind_addr, family, type, proto, nodelay, ssl_adapter, + reuse_port=False, + ): + """Create and prepare the socket object.""" + sock = socket.socket(family, type, proto) + connections.prevent_socket_inheritance(sock) + + host, port = bind_addr[:2] + IS_EPHEMERAL_PORT = port == 0 + + if reuse_port: + cls._make_socket_reusable(socket_=sock, bind_addr=bind_addr) + + if not (IS_WINDOWS or IS_EPHEMERAL_PORT): + """Enable SO_REUSEADDR for the current socket. + + Skip for Windows (has different semantics) + or ephemeral ports (can steal ports from others). + + Refs: + * https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx + * https://github.com/cherrypy/cheroot/issues/114 + * https://gavv.github.io/blog/ephemeral-port-reuse/ + """ + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if nodelay and not isinstance(bind_addr, (str, bytes)): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if ssl_adapter is not None: + sock = ssl_adapter.bind(sock) + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See + # https://github.com/cherrypy/cherrypy/issues/871. + listening_ipv6 = ( + hasattr(socket, 'AF_INET6') + and family == socket.AF_INET6 + and host in ('::', '::0', '::0.0.0.0') + ) + if listening_ipv6: + try: + sock.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0, + ) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + return sock + + @staticmethod + def bind_socket(socket_, bind_addr): + """Bind the socket to given interface.""" + socket_.bind(bind_addr) + return socket_ + + @staticmethod + def resolve_real_bind_addr(socket_): + """Retrieve actual bind address from bound socket.""" + # FIXME: keep requested bind_addr separate real bound_addr (port + # is different in case of ephemeral port 0) + bind_addr = socket_.getsockname() + if socket_.family in ( + # Windows doesn't have socket.AF_UNIX, so not using it in check + socket.AF_INET, + socket.AF_INET6, + ): + """UNIX domain sockets are strings or bytes. + + In case of bytes with a leading null-byte it's an abstract socket. + """ + return bind_addr[:2] + + if isinstance(bind_addr, bytes): + bind_addr = bton(bind_addr) + + return bind_addr + + def process_conn(self, conn): + """Process an incoming HTTPConnection.""" + try: + self.requests.put(conn) + except queue.Full: + # Just drop the conn. TODO: write 503 back? + conn.close() + + @property + def interrupt(self): + """Flag interrupt of the server.""" + return self._interrupt + + @property + def _stopping_for_interrupt(self): + """Return whether the server is responding to an interrupt.""" + return self._interrupt is _STOPPING_FOR_INTERRUPT + + @interrupt.setter + def interrupt(self, interrupt): + """Perform the shutdown of this server and save the exception. + + Typically invoked by a worker thread in + :py:mod:`~cheroot.workers.threadpool`, the exception is raised + from the thread running :py:meth:`serve` once :py:meth:`stop` + has completed. + """ + self._interrupt = _STOPPING_FOR_INTERRUPT + self.stop() + self._interrupt = interrupt + + def stop(self): # noqa: C901 # FIXME + """Gracefully shutdown a server that is serving forever.""" + if not self.ready: + return # already stopped + + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + self._connections.stop() + + sock = getattr(self, 'socket', None) + if sock: + if not isinstance(self.bind_addr, (str, bytes)): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + # Changed to use error code and not message + # See + # https://github.com/cherrypy/cherrypy/issues/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo( + host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, + ): + af, socktype, proto, _canonname, _sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See + # https://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, 'close'): + sock.close() + self.socket = None + + self._connections.close() + self.requests.stop(self.shutdown_timeout) + + +class Gateway: + """Base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + """Initialize Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplementedError # pragma: no cover + + +# These may either be ssl.Adapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter', +} + + +def get_ssl_adapter_class(name='builtin'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, str): + last_dot = adapter.rfind('.') + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError( + "'%s' object has no attribute '%s'" + % (mod_path, attr_name), + ) + + return adapter diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/server.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/server.pyi new file mode 100644 index 000000000..ecbe2f275 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/server.pyi @@ -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 = ...): ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.py b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.py new file mode 100644 index 000000000..19b587d0b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.py @@ -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 diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.pyi new file mode 100644 index 000000000..4801fbdd6 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__init__.pyi @@ -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 = ...): ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..7e011d138 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/builtin.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/builtin.cpython-310.pyc new file mode 100644 index 000000000..468ad1c99 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/builtin.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/pyopenssl.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/pyopenssl.cpython-310.pyc new file mode 100644 index 000000000..eeab5ec12 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/__pycache__/pyopenssl.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.py b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.py new file mode 100644 index 000000000..b22d4ae61 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.py @@ -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) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.pyi new file mode 100644 index 000000000..72e450017 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/builtin.pyi @@ -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 = ...): ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.py b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.py new file mode 100644 index 000000000..548200f7d --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.py @@ -0,0 +1,376 @@ +""" +A library for integrating :doc:`pyOpenSSL ` with Cheroot. + +The :py:mod:`OpenSSL ` module must be importable +for SSL/TLS/HTTPS functionality. +You can obtain it from `here `_. + +To use this module, set :py:attr:`HTTPServer.ssl_adapter +` to an instance of +:py:class:`ssl.Adapter `. +There are two ways to use :abbr:`TLS (Transport-Level Security)`: + +Method One +---------- + + * :py:attr:`ssl_adapter.context + `: an instance of + :py:class:`SSL.Context `. + +If this is not None, it is assumed to be an :py:class:`SSL.Context +` instance, and will be passed to +:py:class:`SSL.Connection ` on bind(). +The developer is responsible for forming a valid :py:class:`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 ` +is desired instead of the default :py:data:`SSL.SSLv23_METHOD +`, etc. Consult +the :doc:`pyOpenSSL ` documentation for +complete options. + +Method Two (shortcut) +--------------------- + + * :py:attr:`ssl_adapter.certificate + `: the file name + of the server's TLS certificate. + * :py:attr:`ssl_adapter.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 +` 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) \ + ` + """ + + 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 `. + """ + + 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 ` + """ + # 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) == "" + 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) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.pyi new file mode 100644 index 000000000..107675c9b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/ssl/pyopenssl.pyi @@ -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: ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__init__.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__init__.py new file mode 100644 index 000000000..e2a7b348e --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__init__.py @@ -0,0 +1 @@ +"""Cheroot test suite.""" diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..50d851028 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/_pytest_plugin.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/_pytest_plugin.cpython-310.pyc new file mode 100644 index 000000000..ab5bbb049 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/_pytest_plugin.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/conftest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/conftest.cpython-310.pyc new file mode 100644 index 000000000..c49539d80 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/conftest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/helper.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/helper.cpython-310.pyc new file mode 100644 index 000000000..9ef5a74d8 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/helper.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test__compat.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test__compat.cpython-310.pyc new file mode 100644 index 000000000..daa6a5c88 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test__compat.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_cli.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_cli.cpython-310.pyc new file mode 100644 index 000000000..cee56fa21 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_cli.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_conn.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_conn.cpython-310.pyc new file mode 100644 index 000000000..2a128d472 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_conn.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_core.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_core.cpython-310.pyc new file mode 100644 index 000000000..5afcc910d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_core.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_dispatch.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_dispatch.cpython-310.pyc new file mode 100644 index 000000000..815101bc3 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_dispatch.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_errors.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_errors.cpython-310.pyc new file mode 100644 index 000000000..e9f2eaea0 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_errors.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_makefile.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_makefile.cpython-310.pyc new file mode 100644 index 000000000..0e3243cf8 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_makefile.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_server.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_server.cpython-310.pyc new file mode 100644 index 000000000..8c031f602 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_server.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_ssl.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_ssl.cpython-310.pyc new file mode 100644 index 000000000..af45091dd Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_ssl.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_wsgi.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_wsgi.cpython-310.pyc new file mode 100644 index 000000000..a2039c233 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/test_wsgi.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/webtest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/webtest.cpython-310.pyc new file mode 100644 index 000000000..bf39d56a5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/test/__pycache__/webtest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/_pytest_plugin.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/_pytest_plugin.py new file mode 100644 index 000000000..61f2efe12 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/_pytest_plugin.py @@ -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. ' + ' dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError( + '%r and %r are not within %r seconds.' % + (dt1, dt2, seconds), + ) + + +class Request: + """HTTP request container.""" + + def __init__(self, environ): + """Initialize HTTP request.""" + self.environ = environ + + +class Response: + """HTTP response container.""" + + def __init__(self): + """Initialize HTTP response.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.body = None + + def output(self): + """Generate iterable response body object.""" + if self.body is None: + return [] + elif isinstance(self.body, str): + return [self.body.encode('iso-8859-1')] + elif isinstance(self.body, bytes): + return [self.body] + else: + return [x.encode('iso-8859-1') for x in self.body] + + +class Controller: + """WSGI app for tests.""" + + def __call__(self, environ, start_response): + """WSGI request handler.""" + req, resp = Request(environ), Response() + try: + # Python 3 supports unicode attribute names + # Python 2 encodes them + handler = self.handlers[environ['PATH_INFO']] + except KeyError: + resp.status = '404 Not Found' + else: + output = handler(req, resp) + if ( + output is not None + and not any( + resp.status.startswith(status_code) + for status_code in ('204', '304') + ) + ): + resp.body = output + try: + resp.headers.setdefault('Content-Length', str(len(output))) + except TypeError: + if not isinstance(output, types.GeneratorType): + raise + start_response(resp.status, resp.headers.items()) + return resp.output() diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test__compat.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test__compat.py new file mode 100644 index 000000000..8a0f3304e --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test__compat.py @@ -0,0 +1,61 @@ +"""Test suite for cross-python compatibility helpers.""" + +import pytest + +from cheroot._compat import extract_bytes, ntob, ntou, bton + + +@pytest.mark.parametrize( + ('func', 'inp', 'out'), + ( + (ntob, 'bar', b'bar'), + (ntou, 'bar', u'bar'), + (bton, b'bar', 'bar'), + ), +) +def test_compat_functions_positive(func, inp, out): + """Check that compatibility functions work with correct input.""" + assert func(inp, encoding='utf-8') == out + + +@pytest.mark.parametrize( + 'func', + ( + ntob, + ntou, + ), +) +def test_compat_functions_negative_nonnative(func): + """Check that compatibility functions fail loudly for incorrect input.""" + non_native_test_str = b'bar' + with pytest.raises(TypeError): + func(non_native_test_str, encoding='utf-8') + + +def test_ntou_escape(): + """Check that ``ntou`` supports escape-encoding under Python 2.""" + expected = u'hišřії' + actual = ntou('hi\u0161\u0159\u0456\u0457', encoding='escape') + assert actual == expected + + +@pytest.mark.parametrize( + ('input_argument', 'expected_result'), + ( + (b'qwerty', b'qwerty'), + (memoryview(b'asdfgh'), b'asdfgh'), + ), +) +def test_extract_bytes(input_argument, expected_result): + """Check that legitimate inputs produce bytes.""" + assert extract_bytes(input_argument) == expected_result + + +def test_extract_bytes_invalid(): + """Ensure that invalid input causes exception to be raised.""" + with pytest.raises( + ValueError, + match=r'^extract_bytes\(\) only accepts bytes ' + 'and memoryview/buffer$', + ): + extract_bytes(u'some юнікод їїї') diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_cli.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_cli.py new file mode 100644 index 000000000..a50ba98ba --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_cli.py @@ -0,0 +1,89 @@ +"""Tests to verify the command line interface. + +.. spelling:: + + cli +""" +import sys + +import pytest + +from cheroot.cli import ( + Application, + parse_wsgi_bind_addr, +) + + +@pytest.mark.parametrize( + ('raw_bind_addr', 'expected_bind_addr'), + ( + # tcp/ip + ('192.168.1.1:80', ('192.168.1.1', 80)), + # ipv6 ips has to be enclosed in brakets when specified in url form + ('[::1]:8000', ('::1', 8000)), + ('localhost:5000', ('localhost', 5000)), + # this is a valid input, but foo gets discarted + ('foo@bar:5000', ('bar', 5000)), + ('foo', ('foo', None)), + ('123456789', ('123456789', None)), + # unix sockets + ('/tmp/cheroot.sock', '/tmp/cheroot.sock'), + ('/tmp/some-random-file-name', '/tmp/some-random-file-name'), + # abstract sockets + ('@cheroot', '\x00cheroot'), + ), +) +def test_parse_wsgi_bind_addr(raw_bind_addr, expected_bind_addr): + """Check the parsing of the --bind option. + + Verify some of the supported addresses and the expected return value. + """ + assert parse_wsgi_bind_addr(raw_bind_addr) == expected_bind_addr + + +@pytest.fixture +def wsgi_app(monkeypatch): + """Return a WSGI app stub.""" + class WSGIAppMock: + """Mock of a wsgi module.""" + + def application(self): + """Empty application method. + + Default method to be called when no specific callable + is defined in the wsgi application identifier. + + It has an empty body because we are expecting to verify that + the same method is return no the actual execution of it. + """ + + def main(self): + """Empty custom method (callable) inside the mocked WSGI app. + + It has an empty body because we are expecting to verify that + the same method is return no the actual execution of it. + """ + app = WSGIAppMock() + # patch sys.modules, to include the an instance of WSGIAppMock + # under a specific namespace + monkeypatch.setitem(sys.modules, 'mypkg.wsgi', app) + return app + + +@pytest.mark.parametrize( + ('app_name', 'app_method'), + ( + (None, 'application'), + ('application', 'application'), + ('main', 'main'), + ), +) +# pylint: disable=invalid-name +def test_Aplication_resolve(app_name, app_method, wsgi_app): + """Check the wsgi application name conversion.""" + if app_name is None: + wsgi_app_spec = 'mypkg.wsgi' + else: + wsgi_app_spec = 'mypkg.wsgi:{app_name}'.format(**locals()) + expected_app = getattr(wsgi_app, app_method) + assert Application.resolve(wsgi_app_spec).wsgi_app == expected_app diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_conn.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_conn.py new file mode 100644 index 000000000..f2d2cdb20 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_conn.py @@ -0,0 +1,1310 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import errno +import socket +import time +import logging +import traceback as traceback_ +from collections import namedtuple +import http.client +import urllib.request + +import pytest +from jaraco.text import trim, unwrap + +from cheroot.test import helper, webtest +from cheroot._compat import IS_CI, IS_MACOS, IS_PYPY, IS_WINDOWS +import cheroot.server + + +IS_SLOW_ENV = IS_MACOS or IS_WINDOWS + + +timeout = 1 +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + + +class Controller(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello, world!' + + def pov(req, resp): + """Render ``pov`` value.""" + return pov + + def stream(req, resp): + """Render streaming response.""" + if 'set_cl' in req.environ['QUERY_STRING']: + resp.headers['Content-Length'] = str(10) + + def content(): + for x in range(10): + yield str(x) + + return content() + + def upload(req, resp): + """Process file upload and render thank.""" + if not req.environ['REQUEST_METHOD'] == 'POST': + raise AssertionError( + "'POST' != request.method %r" % + req.environ['REQUEST_METHOD'], + ) + return "thanks for '%s'" % req.environ['wsgi.input'].read() + + def custom_204(req, resp): + """Render response with status 204.""" + resp.status = '204' + return 'Code = 204' + + def custom_304(req, resp): + """Render response with status 304.""" + resp.status = '304' + return 'Code = 304' + + def err_before_read(req, resp): + """Render response with status 500.""" + resp.status = '500 Internal Server Error' + return 'ok' + + def one_megabyte_of_a(req, resp): + """Render 1MB response.""" + return ['a' * 1024] * 1024 + + def wrong_cl_buffered(req, resp): + """Render buffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return 'I have too many bytes' + + def wrong_cl_unbuffered(req, resp): + """Render unbuffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return ['I too', ' have too many bytes'] + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + return string.encode('utf-8').decode('latin-1') + + handlers = { + '/hello': hello, + '/pov': pov, + '/page1': pov, + '/page2': pov, + '/page3': pov, + '/stream': stream, + '/upload': upload, + '/custom/204': custom_204, + '/custom/304': custom_304, + '/err_before_read': err_before_read, + '/one_megabyte_of_a': one_megabyte_of_a, + '/wrong_cl_buffered': wrong_cl_buffered, + '/wrong_cl_unbuffered': wrong_cl_unbuffered, + } + + +class ErrorLogMonitor: + """Mock class to access the server error_log calls made by the server.""" + + ErrorLogCall = namedtuple('ErrorLogCall', ['msg', 'level', 'traceback']) + + def __init__(self): + """Initialize the server error log monitor/interceptor. + + If you need to ignore a particular error message use the property + ``ignored_msgs`` by appending to the list the expected error messages. + """ + self.calls = [] + # to be used the the teardown validation + self.ignored_msgs = [] + + def __call__(self, msg='', level=logging.INFO, traceback=False): + """Intercept the call to the server error_log method.""" + if traceback: + tblines = traceback_.format_exc() + else: + tblines = '' + self.calls.append(ErrorLogMonitor.ErrorLogCall(msg, level, tblines)) + + +@pytest.fixture +def raw_testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and preconfigure it.""" + app = Controller() + + def _timeout(req, resp): + return str(wsgi_server.timeout) + app.handlers['/timeout'] = _timeout + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = app + wsgi_server.max_request_body_size = 1001 + wsgi_server.timeout = timeout + wsgi_server.server_client = wsgi_server_client + wsgi_server.keep_alive_conn_limit = 2 + + return wsgi_server + + +@pytest.fixture +def testing_server(raw_testing_server, monkeypatch): + """Modify the "raw" base server to monitor the error_log messages. + + If you need to ignore a particular error message use the property + ``testing_server.error_log.ignored_msgs`` by appending to the list + the expected error messages. + """ + # patch the error_log calls of the server instance + monkeypatch.setattr(raw_testing_server, 'error_log', ErrorLogMonitor()) + + yield raw_testing_server + + # Teardown verification, in case that the server logged an + # error that wasn't notified to the client or we just made a mistake. + # pylint: disable=possibly-unused-variable + for c_msg, c_level, c_traceback in raw_testing_server.error_log.calls: + if c_level <= logging.WARNING: + continue + + assert c_msg in raw_testing_server.error_log.ignored_msgs, ( + 'Found error in the error log: ' + "message = '{c_msg}', level = '{c_level}'\n" + '{c_traceback}'.format(**locals()), + ) + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +def header_exists(header_name, headers): + """Check that a header is present.""" + return header_name.lower() in (k.lower() for (k, _) in headers) + + +def header_has_value(header_name, header_value, headers): + """Check that a header with a given value is present.""" + return header_name.lower() in ( + k.lower() for (k, v) in headers + if v == header_value + ) + + +def test_HTTP11_persistent_connections(test_client): + """Test persistent HTTP/1.1 connections.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make another request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page1', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test client-side close. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', http_conn=http_connection, + headers=[('Connection', 'close')], + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http.client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ), +) +def test_streaming_11(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.1 protocol.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection, + ) + assert header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'close', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection, + ) + assert not header_exists('Content-Length', actual_headers) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + chunked_response = False + for k, v in actual_headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + assert not header_has_value('Connection', 'close', actual_headers) + else: + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should + # error. + with pytest.raises(http.client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + # Try HEAD. + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864. + # TODO: figure out how can this be possible on an closed connection + # (chunked_response case) + status_line, actual_headers, actual_resp_body = test_client.head( + '/stream', http_conn=http_connection, + ) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'' + assert not header_exists('Transfer-Encoding', actual_headers) + + # Prevent the resource warnings: + http_connection.close() + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ), +) +def test_streaming_10(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.0 protocol.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert Keep-Alive. + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert header_exists('Content-Length', actual_headers) + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + else: + # When a Content-Length is not provided, + # the server should close the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert not header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http.client.NotConnected): + test_client.get( + '/pov', http_conn=http_connection, + protocol='HTTP/1.0', + ) + + test_client.server_instance.protocol = original_server_protocol + + # Prevent the resource warnings: + http_connection.close() + + +@pytest.mark.parametrize( + 'http_server_protocol', + ( + 'HTTP/1.0', + pytest.param( + 'HTTP/1.1', + marks=pytest.mark.xfail( + IS_PYPY and IS_CI, + reason='Fails under PyPy in CI for unknown reason', + strict=False, + ), + ), + ), +) +def test_keepalive(test_client, http_server_protocol): + """Test Keep-Alive enabled connections.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = http_server_protocol + + http_client_protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Test a normal HTTP/1.0 request. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test a keep-alive HTTP/1.0 request. + + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', headers=[('Connection', 'Keep-Alive')], + http_conn=http_connection, protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + assert header_has_value( + 'Keep-Alive', + 'timeout={test_client.server_instance.timeout}'.format(**locals()), + actual_headers, + ) + + # Remove the keep-alive header again. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', http_conn=http_connection, + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + assert not header_exists('Keep-Alive', actual_headers) + + test_client.server_instance.protocol = original_server_protocol + + # Prevent the resource warnings: + http_connection.close() + + +def test_keepalive_conn_management(test_client): + """Test management of Keep-Alive connections.""" + test_client.server_instance.timeout = 2 + + def connection(): + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + return http_connection + + def request(conn, keepalive=True): + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', headers=[('Connection', 'Keep-Alive')], + http_conn=conn, protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + if keepalive: + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + assert header_has_value( + 'Keep-Alive', + 'timeout={test_client.server_instance.timeout}'. + format(**locals()), + actual_headers, + ) + else: + assert not header_exists('Connection', actual_headers) + assert not header_exists('Keep-Alive', actual_headers) + + def check_server_idle_conn_count(count, timeout=1.0): + deadline = time.time() + timeout + while True: + n = test_client.server_instance._connections._num_connections + if n == count: + return + assert time.time() <= deadline, ( + 'idle conn count mismatch, wanted {count}, got {n}'. + format(**locals()), + ) + + disconnect_errors = ( + http.client.BadStatusLine, + http.client.CannotSendRequest, + http.client.NotConnected, + ) + + # Make a new connection. + c1 = connection() + request(c1) + check_server_idle_conn_count(1) + + # Make a second one. + c2 = connection() + request(c2) + check_server_idle_conn_count(2) + + # Reusing the first connection should still work. + request(c1) + check_server_idle_conn_count(2) + + # Creating a new connection should still work, but we should + # have run out of available connections to keep alive, so the + # server should tell us to close. + c3 = connection() + request(c3, keepalive=False) + check_server_idle_conn_count(2) + + # Show that the third connection was closed. + with pytest.raises(disconnect_errors): + request(c3) + check_server_idle_conn_count(2) + + # Wait for some of our timeout. + time.sleep(1.2) + + # Refresh the second connection. + request(c2) + check_server_idle_conn_count(2) + + # Wait for the remainder of our timeout, plus one tick. + time.sleep(1.2) + check_server_idle_conn_count(1) + + # First connection should now be expired. + with pytest.raises(disconnect_errors): + request(c1) + check_server_idle_conn_count(1) + + # But the second one should still be valid. + request(c2) + check_server_idle_conn_count(1) + + # Restore original timeout. + test_client.server_instance.timeout = timeout + + # Prevent the resource warnings: + c1.close() + c2.close() + c3.close() + + +@pytest.mark.parametrize( + ('simulated_exception', 'error_number', 'exception_leaks'), + ( + pytest.param( + socket.error, errno.ECONNRESET, False, + id='socket.error(ECONNRESET)', + ), + pytest.param( + socket.error, errno.EPIPE, False, + id='socket.error(EPIPE)', + ), + pytest.param( + socket.error, errno.ENOTCONN, False, + id='simulated socket.error(ENOTCONN)', + ), + pytest.param( + None, # <-- don't raise an artificial exception + errno.ENOTCONN, False, + id='real socket.error(ENOTCONN)', + marks=pytest.mark.xfail( + IS_WINDOWS, + reason='Now reproducible this way on Windows', + ), + ), + pytest.param( + socket.error, errno.ESHUTDOWN, False, + id='socket.error(ESHUTDOWN)', + ), + pytest.param(RuntimeError, 666, True, id='RuntimeError(666)'), + pytest.param(socket.error, -1, True, id='socket.error(-1)'), + ) + ( + pytest.param( + ConnectionResetError, errno.ECONNRESET, False, + id='ConnectionResetError(ECONNRESET)', + ), + pytest.param( + BrokenPipeError, errno.EPIPE, False, + id='BrokenPipeError(EPIPE)', + ), + pytest.param( + BrokenPipeError, errno.ESHUTDOWN, False, + id='BrokenPipeError(ESHUTDOWN)', + ), + ), +) +def test_broken_connection_during_tcp_fin( + error_number, exception_leaks, + mocker, monkeypatch, + simulated_exception, test_client, +): + """Test there's no traceback on broken connection during close. + + It artificially causes :py:data:`~errno.ECONNRESET` / + :py:data:`~errno.EPIPE` / :py:data:`~errno.ESHUTDOWN` / + :py:data:`~errno.ENOTCONN` as well as unrelated :py:exc:`RuntimeError` + and :py:exc:`socket.error(-1) ` on the server socket when + :py:meth:`socket.shutdown() ` is called. It's + triggered by closing the client socket before the server had a chance + to respond. + + The expectation is that only :py:exc:`RuntimeError` and a + :py:exc:`socket.error` with an unusual error code would leak. + + With the :py:data:`None`-parameter, a real non-simulated + :py:exc:`OSError(107, 'Transport endpoint is not connected') + ` happens. + """ + exc_instance = ( + None if simulated_exception is None + else simulated_exception(error_number, 'Simulated socket error') + ) + old_close_kernel_socket = ( + test_client.server_instance. + ConnectionClass._close_kernel_socket + ) + + def _close_kernel_socket(self): + monkeypatch.setattr( # `socket.shutdown` is read-only otherwise + self, 'socket', + mocker.mock_module.Mock(wraps=self.socket), + ) + if exc_instance is not None: + monkeypatch.setattr( + self.socket, 'shutdown', + mocker.mock_module.Mock(side_effect=exc_instance), + ) + _close_kernel_socket.fin_spy = mocker.spy(self.socket, 'shutdown') + + try: + old_close_kernel_socket(self) + except simulated_exception: + _close_kernel_socket.exception_leaked = True + else: + _close_kernel_socket.exception_leaked = False + + monkeypatch.setattr( + test_client.server_instance.ConnectionClass, + '_close_kernel_socket', + _close_kernel_socket, + ) + + conn = test_client.get_connection() + conn.auto_open = False + conn.connect() + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % conn.host).encode('ascii')) + conn.close() + + # Let the server attempt TCP shutdown: + for _ in range(10 * (2 if IS_SLOW_ENV else 1)): + time.sleep(0.1) + if hasattr(_close_kernel_socket, 'exception_leaked'): + break + + if exc_instance is not None: # simulated by us + assert _close_kernel_socket.fin_spy.spy_exception is exc_instance + else: # real + assert isinstance( + _close_kernel_socket.fin_spy.spy_exception, socket.error, + ) + assert _close_kernel_socket.fin_spy.spy_exception.errno == error_number + + assert _close_kernel_socket.exception_leaked is exception_leaks + + +@pytest.mark.parametrize( + 'timeout_before_headers', + ( + True, + False, + ), +) +def test_HTTP11_Timeout(test_client, timeout_before_headers): + """Check timeout without sending any data. + + The server will close the connection with a 408. + """ + conn = test_client.get_connection() + conn.auto_open = False + conn.connect() + + if not timeout_before_headers: + # Connect but send half the headers only. + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % conn.host).encode('ascii')) + # else: Connect but send nothing. + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 408 + conn.close() + + +def test_HTTP11_Timeout_after_request(test_client): + """Check timeout after at least one request has succeeded. + + The server should close the connection without 408. + """ + fail_msg = "Writing to timed out socket didn't fail as it should have: %s" + + # Make an initial request + conn = test_client.get_connection() + conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = str(timeout).encode() + assert actual_body == expected_body + + # Make a second request on the same socket + conn._output(b'GET /hello HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = b'Hello, world!' + assert actual_body == expected_body + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(b'GET /hello HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except (socket.error, http.client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Make another request on a new socket, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + + # Make another request on the same socket, + # but timeout on the headers + conn.send(b'GET /hello HTTP/1.1') + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except (socket.error, http.client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + conn.close() + + +def test_HTTP11_pipelining(test_client): + """Test HTTP/1.1 pipelining. + + :py:mod:`http.client` doesn't support this directly. + """ + conn = test_client.get_connection() + + # Put request 1 + conn.putrequest('GET', '/hello', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output( + ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1'), + ) + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method='GET') + # there is a bug in python3 regarding the buffering of + # ``conn.sock``. Until that bug get's fixed we will + # monkey patch the ``response`` instance. + # https://bugs.python.org/issue23377 + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + assert response.status == 200 + assert body == b'Hello, world!' + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + assert response.status == 200 + assert body == b'Hello, world!' + + conn.close() + + +def test_100_Continue(test_client): + """Test 100-continue header processing.""" + conn = test_client.get_connection() + + # Try a page without an Expect request header first. + # Note that http.client's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conn.send(b"d'oh") + response = conn.response_class(conn.sock, method='POST') + _version, status, _reason = response._read_status() + assert status != 100 + conn.close() + + # Now try a page with an Expect header... + conn.connect() + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '17') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + while True: + line = response.fp.readline().strip() + if line: + pytest.fail( + '100 Continue should not output any headers. Got %r' % + line, + ) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, _actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + +@pytest.mark.parametrize( + 'max_request_body_size', + ( + 0, + 1001, + ), +) +def test_readall_or_close(test_client, max_request_body_size): + """Test a max_request_body_size of 0 (the default) and 1001.""" + old_max = test_client.server_instance.max_request_body_size + + test_client.server_instance.max_request_body_size = max_request_body_size + + conn = test_client.get_connection() + + # Get a POST page with an error + conn.putrequest('POST', '/err_before_read', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '1000') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + _version, status, _reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + conn.send(b'x' * 1000) + + # ...get the final response + response.begin() + status_line, _actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 500 + + # Now try a working page with an Expect header... + conn._output(b'POST /upload HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._output(b'Content-Type: text/plain') + conn._output(b'Content-Length: 17') + conn._output(b'Expect: 100-continue') + conn._send_output() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + test_client.server_instance.max_request_body_size = old_max + + +def test_No_Message_Body(test_client): + """Test HTTP queries with an empty response body.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make a 204 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/204', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 204 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + # Make a 304 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/304', http_conn=http_connection, + ) + actual_status = int(status_line[:3]) + assert actual_status == 304 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + # Prevent the resource warnings: + http_connection.close() + + +@pytest.mark.xfail( + reason=unwrap( + trim(""" + Headers from earlier request leak into the request + line for a subsequent request, resulting in 400 + instead of 413. See cherrypy/cheroot#69 for details. + """), + ), +) +def test_Chunked_Encoding(test_client): + """Test HTTP uploads with chunked transfer-encoding.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + # Try a normal chunked request (with extensions) + body = ( + b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' + b'Content-Type: application/json\r\n' + b'\r\n' + ) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Trailer', 'Content-Type') + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader('Content-Length', '3') + conn.endheaders() + conn.send(body) + response = conn.getresponse() + status_line, _actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode() + assert actual_resp_body == expected_resp_body + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = b'\r\n'.join((b'3e3', b'x' * 995, b'0', b'', b'')) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Content-Type', 'text/plain') + # Chunked requests don't need a content-length + # conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + conn.close() + + +def test_Content_Length_in(test_client): + """Try a non-chunked request where Content-Length exceeds limit. + + (server.max_request_body_size). + Assert error before body send. + """ + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '9999') + conn.endheaders() + response = conn.getresponse() + status_line, _actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + expected_resp_body = ( + b'The entity sent with the request exceeds ' + b'the maximum allowed bytes.' + ) + assert actual_resp_body == expected_resp_body + conn.close() + + +def test_Content_Length_not_int(test_client): + """Test that malicious Content-Length header returns 400.""" + status_line, _actual_headers, actual_resp_body = test_client.post( + '/upload', + headers=[ + ('Content-Type', 'text/plain'), + ('Content-Length', 'not-an-integer'), + ], + ) + actual_status = int(status_line[:3]) + + assert actual_status == 400 + assert actual_resp_body == b'Malformed Content-Length Header.' + + +@pytest.mark.parametrize( + ('uri', 'expected_resp_status', 'expected_resp_body'), + ( + ( + '/wrong_cl_buffered', 500, + ( + b'The requested resource returned more bytes than the ' + b'declared Content-Length.' + ), + ), + ('/wrong_cl_unbuffered', 200, b'I too'), + ), +) +def test_Content_Length_out( + test_client, + uri, expected_resp_status, expected_resp_body, +): + """Test response with Content-Length less than the response body. + + (non-chunked response) + """ + conn = test_client.get_connection() + conn.putrequest('GET', uri, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + response = conn.getresponse() + status_line, _actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + + assert actual_status == expected_resp_status + assert actual_resp_body == expected_resp_body + + conn.close() + + # the server logs the exception that we had verified from the + # client perspective. Tell the error_log verification that + # it can ignore that message. + test_client.server_instance.error_log.ignored_msgs.extend(( + # Python 3.7+: + "ValueError('Response body exceeds the declared Content-Length.')", + # Python 2.7-3.6 (macOS?): + "ValueError('Response body exceeds the declared Content-Length.',)", + )) + + +@pytest.mark.xfail( + reason='Sometimes this test fails due to low timeout. ' + 'Ref: https://github.com/cherrypy/cherrypy/issues/598', +) +def test_598(test_client): + """Test serving large file with a read timeout in place.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + remote_data_conn = urllib.request.urlopen( + '%s://%s:%s/one_megabyte_of_a' + % ('http', conn.host, conn.port), + ) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + buf += data + remaining -= len(data) + + assert len(buf) == 1024 * 1024 + assert buf == b'a' * 1024 * 1024 + assert remaining == 0 + remote_data_conn.close() + + +@pytest.mark.parametrize( + 'invalid_terminator', + ( + b'\n\n', + b'\r\n\n', + ), +) +def test_No_CRLF(test_client, invalid_terminator): + """Test HTTP queries with no valid CRLF terminators.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + conn.send(b'GET /hello HTTP/1.1%s' % invalid_terminator) + response = conn.response_class(conn.sock, method='GET') + response.begin() + actual_resp_body = response.read() + expected_resp_body = b'HTTP requires CRLF terminators' + assert actual_resp_body == expected_resp_body + conn.close() + + +class FaultySelect: + """Mock class to insert errors in the selector.select method.""" + + def __init__(self, original_select): + """Initilize helper class to wrap the selector.select method.""" + self.original_select = original_select + self.request_served = False + self.os_error_triggered = False + + def __call__(self, timeout): + """Intercept the calls to selector.select.""" + if self.request_served: + self.os_error_triggered = True + raise OSError('Error while selecting the client socket.') + + return self.original_select(timeout) + + +class FaultyGetMap: + """Mock class to insert errors in the selector.get_map method.""" + + def __init__(self, original_get_map): + """Initilize helper class to wrap the selector.get_map method.""" + self.original_get_map = original_get_map + self.sabotage_conn = False + self.conn_closed = False + + def __call__(self): + """Intercept the calls to selector.get_map.""" + sabotage_targets = ( + conn for _, (_, _, _, conn) in self.original_get_map().items() + if isinstance(conn, cheroot.server.HTTPConnection) + ) if self.sabotage_conn and not self.conn_closed else () + + for conn in sabotage_targets: + # close the socket to cause OSError + conn.close() + self.conn_closed = True + + return self.original_get_map() + + +def test_invalid_selected_connection(test_client, monkeypatch): + """Test the error handling segment of HTTP connection selection. + + See :py:meth:`cheroot.connections.ConnectionManager.get_conn`. + """ + # patch the select method + faux_select = FaultySelect( + test_client.server_instance._connections._selector.select, + ) + monkeypatch.setattr( + test_client.server_instance._connections._selector, + 'select', + faux_select, + ) + + # patch the get_map method + faux_get_map = FaultyGetMap( + test_client.server_instance._connections._selector._selector.get_map, + ) + + monkeypatch.setattr( + test_client.server_instance._connections._selector._selector, + 'get_map', + faux_get_map, + ) + + # request a page with connection keep-alive to make sure + # we'll have a connection to be modified. + resp_status, _resp_headers, _resp_body = test_client.request( + '/page1', headers=[('Connection', 'Keep-Alive')], + ) + + assert resp_status == '200 OK' + # trigger the internal errors + faux_get_map.sabotage_conn = faux_select.request_served = True + # give time to make sure the error gets handled + time.sleep(test_client.server_instance.expiration_interval * 2) + assert faux_select.os_error_triggered + assert faux_get_map.conn_closed diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_core.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_core.py new file mode 100644 index 000000000..7732b6f7a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_core.py @@ -0,0 +1,455 @@ +"""Tests for managing HTTP issues (malformed requests, etc).""" + +import errno +import socket +import urllib.parse # noqa: WPS301 + +import pytest + +from cheroot.test import helper + + +HTTP_BAD_REQUEST = 400 +HTTP_LENGTH_REQUIRED = 411 +HTTP_NOT_FOUND = 404 +HTTP_REQUEST_ENTITY_TOO_LARGE = 413 +HTTP_OK = 200 +HTTP_VERSION_NOT_SUPPORTED = 505 + + +class HelloController(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello world!' + + def body_required(req, resp): + """Render Hello world or set 411.""" + if req.environ.get('Content-Length', None) is None: + resp.status = '411 Length Required' + return + return 'Hello world!' + + def query_string(req, resp): + """Render QUERY_STRING value.""" + return req.environ.get('QUERY_STRING', '') + + def asterisk(req, resp): + """Render request method value.""" + # pylint: disable=possibly-unused-variable + method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND') + tmpl = 'Got asterisk URI path with {method} method' + return tmpl.format(**locals()) + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + return string.encode('utf-8').decode('latin-1') + + handlers = { + '/hello': hello, + '/no_body': hello, + '/body_required': body_required, + '/query_string': query_string, + # FIXME: Unignore the pylint rules in pylint >= 2.15.4. + # Refs: + # * https://github.com/PyCQA/pylint/issues/6592 + # * https://github.com/PyCQA/pylint/pull/7395 + # pylint: disable-next=too-many-function-args + _munge('/привіт'): hello, + # pylint: disable-next=too-many-function-args + _munge('/Юххууу'): hello, + '/\xa0Ðblah key 0 900 4 data': hello, + '/*': asterisk, + } + + +def _get_http_response(connection, method='GET'): + return connection.response_class(connection.sock, method=method) + + +@pytest.fixture +def testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and preconfigure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = HelloController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +@pytest.fixture +def testing_server_with_defaults(wsgi_server_client): + """Attach a WSGI app to the given server and preconfigure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = HelloController() + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client_with_defaults(testing_server_with_defaults): + """Get and return a test client out of the given server.""" + return testing_server_with_defaults.server_client + + +def test_http_connect_request(test_client): + """Check that CONNECT query results in Method Not Allowed status.""" + status_line = test_client.connect('/anything')[0] + actual_status = int(status_line[:3]) + assert actual_status == 405 + + +def test_normal_request(test_client): + """Check that normal GET query succeeds.""" + status_line, _, actual_resp_body = test_client.get('/hello') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + +def test_query_string_request(test_client): + """Check that GET param is parsed well.""" + status_line, _, actual_resp_body = test_client.get( + '/query_string?test=True', + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'test=True' + + +@pytest.mark.parametrize( + 'uri', + ( + '/hello', # plain + '/query_string?test=True', # query + '/{0}?{1}={2}'.format( # quoted unicode + *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо')) + ), + ), +) +def test_parse_acceptable_uri(test_client, uri): + """Check that server responds with OK to valid GET queries.""" + status_line = test_client.get(uri)[0] + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + + +def test_parse_uri_unsafe_uri(test_client): + """Test that malicious URI does not allow HTTP injection. + + This effectively checks that sending GET request with URL + + /%A0%D0blah%20key%200%20900%204%20data + + is not converted into + + GET / + blah key 0 900 4 data + HTTP/1.1 + + which would be a security issue otherwise. + """ + c = test_client.get_connection() + resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1') + quoted = urllib.parse.quote(resource) + assert quoted == '/%A0%D0blah%20key%200%20900%204%20data' + request = 'GET {quoted} HTTP/1.1'.format(**locals()) + c._output(request.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_OK + assert response.read(12) == b'Hello world!' + c.close() + + +def test_parse_uri_invalid_uri(test_client): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should only contain US-ASCII. + """ + c = test_client.get_connection() + c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_BAD_REQUEST + assert response.read(21) == b'Malformed Request-URI' + c.close() + + +@pytest.mark.parametrize( + 'uri', + ( + 'hello', # ascii + 'привіт', # non-ascii + ), +) +def test_parse_no_leading_slash_invalid(test_client, uri): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should have leading slash (be absolute). + """ + status_line, _, actual_resp_body = test_client.get( + urllib.parse.quote(uri), + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + assert b'starting with a slash' in actual_resp_body + + +def test_parse_uri_absolute_uri(test_client): + """Check that server responds with Bad Request to Absolute URI. + + Only proxy servers should allow this. + """ + status_line, _, actual_resp_body = test_client.get('http://google.com/') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Absolute URI not allowed if server is not a proxy.' + assert actual_resp_body == expected_body + + +def test_parse_uri_asterisk_uri(test_client): + """Check that server responds with OK to OPTIONS with "*" Absolute URI.""" + status_line, _, actual_resp_body = test_client.options('*') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + expected_body = b'Got asterisk URI path with OPTIONS method' + assert actual_resp_body == expected_body + + +def test_parse_uri_fragment_uri(test_client): + """Check that server responds with Bad Request to URI with fragment.""" + status_line, _, actual_resp_body = test_client.get( + '/hello?test=something#fake', + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Illegal #fragment in Request-URI.' + assert actual_resp_body == expected_body + + +def test_no_content_length(test_client): + """Test POST query with an empty body being successful.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. + c = test_client.get_connection() + c.request('POST', '/no_body') + response = c.getresponse() + actual_resp_body = response.read() + actual_status = response.status + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + c.close() # deal with the resource warning + + +def test_content_length_required(test_client): + """Test POST query with body failing because of missing Content-Length.""" + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + + c = test_client.get_connection() + c.request('POST', '/body_required') + response = c.getresponse() + response.read() + + actual_status = response.status + assert actual_status == HTTP_LENGTH_REQUIRED + + c.close() # deal with the resource warning + + +@pytest.mark.xfail( + reason='https://github.com/cherrypy/cheroot/issues/106', + strict=False, # sometimes it passes +) +def test_large_request(test_client_with_defaults): + """Test GET query with maliciously large Content-Length.""" + # If the server's max_request_body_size is not set (i.e. is set to 0) + # then this will result in an `OverflowError: Python int too large to + # convert to C ssize_t` in the server. + # We expect that this should instead return that the request is too + # large. + c = test_client_with_defaults.get_connection() + c.putrequest('GET', '/hello') + c.putheader('Content-Length', str(2**64)) + c.endheaders() + + response = c.getresponse() + actual_status = response.status + + assert actual_status == HTTP_REQUEST_ENTITY_TOO_LARGE + + +@pytest.mark.parametrize( + ('request_line', 'status_code', 'expected_body'), + ( + ( + b'GET /', # missing proto + HTTP_BAD_REQUEST, b'Malformed Request-Line', + ), + ( + b'GET / HTTPS/1.1', # invalid proto + HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol', + ), + ( + b'GET / HTTP/1', # invalid version + HTTP_BAD_REQUEST, b'Malformed Request-Line: bad version', + ), + ( + b'GET / HTTP/2.15', # invalid ver + HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request', + ), + ), +) +def test_malformed_request_line( + test_client, request_line, + status_code, expected_body, +): + """Test missing or invalid HTTP version in Request-Line.""" + c = test_client.get_connection() + c._output(request_line) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == status_code + assert response.read(len(expected_body)) == expected_body + c.close() + + +def test_malformed_http_method(test_client): + """Test non-uppercase HTTP method.""" + c = test_client.get_connection() + c.putrequest('GeT', '/malformed_method_case') + c.putheader('Content-Type', 'text/plain') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.read(21) + assert actual_resp_body == b'Malformed method name' + + c.close() # deal with the resource warning + + +def test_malformed_header(test_client): + """Check that broken HTTP header results in Bad Request.""" + c = test_client.get_connection() + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941 + c._output(b'Re, 1.2.3.4#015#012') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.read(20) + assert actual_resp_body == b'Illegal header line.' + + c.close() # deal with the resource warning + + +def test_request_line_split_issue_1220(test_client): + """Check that HTTP request line of exactly 256 chars length is OK.""" + Request_URI = ( + '/hello?' + 'intervenant-entreprise-evenement_classaction=' + 'evenement-mailremerciements' + '&_path=intervenant-entreprise-evenement' + '&intervenant-entreprise-evenement_action-id=19404' + '&intervenant-entreprise-evenement_id=19404' + '&intervenant-entreprise_id=28092' + ) + assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256 + + actual_resp_body = test_client.get(Request_URI)[2] + assert actual_resp_body == b'Hello world!' + + +def test_garbage_in(test_client): + """Test that server sends an error for garbage received over TCP.""" + # Connect without SSL regardless of server.scheme + + c = test_client.get_connection() + c._output(b'gjkgjklsgjklsgjkljklsg') + c._send_output() + response = c.response_class(c.sock, method='GET') + try: + response.begin() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.read(22) + assert actual_resp_body == b'Malformed Request-Line' + c.close() + except socket.error as ex: + # "Connection reset by peer" is also acceptable. + if ex.errno != errno.ECONNRESET: + raise + + +class CloseController: + """Controller for testing the close callback.""" + + def __call__(self, environ, start_response): + """Get the req to know header sent status.""" + self.req = start_response.__self__.req + resp = CloseResponse(self.close) + start_response(resp.status, resp.headers.items()) + return resp + + def close(self): + """Close, writing hello.""" + self.req.write(b'hello') + + +class CloseResponse: + """Dummy empty response to trigger the no body status.""" + + def __init__(self, close): + """Use some defaults to ensure we have a header.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.close = close + + def __getitem__(self, index): + """Ensure we don't have a body.""" + raise IndexError() + + def output(self): + """Return self to hook the close method.""" + return self + + +@pytest.fixture +def testing_server_close(wsgi_server_client): + """Attach a WSGI app to the given server and preconfigure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = CloseController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +def test_send_header_before_closing(testing_server_close): + """Test we are actually sending the headers before calling 'close'.""" + _, _, resp_body = testing_server_close.server_client.get('/') + assert resp_body == b'hello' diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_dispatch.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_dispatch.py new file mode 100644 index 000000000..c42014fac --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_dispatch.py @@ -0,0 +1,51 @@ +"""Tests for the HTTP server.""" + +from cheroot.wsgi import PathInfoDispatcher + + +def wsgi_invoke(app, environ): + """Serve 1 request from a WSGI application.""" + response = {} + + def start_response(status, headers): + response.update({ + 'status': status, + 'headers': headers, + }) + + response['body'] = b''.join( + app(environ, start_response), + ) + + return response + + +def test_dispatch_no_script_name(): + """Dispatch despite lack of ``SCRIPT_NAME`` in environ.""" + # Bare bones WSGI hello world app (from PEP 333). + def app(environ, start_response): + start_response( + '200 OK', [ + ('Content-Type', 'text/plain; charset=utf-8'), + ], + ) + return [u'Hello, world!'.encode('utf-8')] + + # Build a dispatch table. + d = PathInfoDispatcher([ + ('/', app), + ]) + + # Dispatch a request without `SCRIPT_NAME`. + response = wsgi_invoke( + d, { + 'PATH_INFO': '/foo', + }, + ) + assert response == { + 'status': '200 OK', + 'headers': [ + ('Content-Type', 'text/plain; charset=utf-8'), + ], + 'body': b'Hello, world!', + } diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_errors.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_errors.py new file mode 100644 index 000000000..a5dd5c2b0 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_errors.py @@ -0,0 +1,31 @@ +"""Test suite for ``cheroot.errors``.""" + +import pytest + +from cheroot import errors + +from .._compat import IS_LINUX, IS_MACOS, IS_SOLARIS, IS_WINDOWS # noqa: WPS130 + + +@pytest.mark.parametrize( + ('err_names', 'err_nums'), + ( + (('', 'some-nonsense-name'), []), + ( + ( + 'EPROTOTYPE', 'EAGAIN', 'EWOULDBLOCK', + 'WSAEWOULDBLOCK', 'EPIPE', + ), + (91, 11, 32) if IS_LINUX else + (32, 35, 41) if IS_MACOS else + (98, 11, 32) if IS_SOLARIS else + (32, 10041, 11, 10035) if IS_WINDOWS else + (), + ), + ), +) +def test_plat_specific_errors(err_names, err_nums): + """Test that ``plat_specific_errors`` gets correct error numbers list.""" + actual_err_nums = errors.plat_specific_errors(*err_names) + assert len(actual_err_nums) == len(err_nums) + assert sorted(actual_err_nums) == sorted(err_nums) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_makefile.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_makefile.py new file mode 100644 index 000000000..57f6f57ea --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_makefile.py @@ -0,0 +1,49 @@ +"""Tests for :py:mod:`cheroot.makefile`.""" + +from cheroot import makefile + + +class MockSocket: + """A mock socket.""" + + def __init__(self): + """Initialize :py:class:`MockSocket`.""" + self.messages = [] + + def recv_into(self, buf): + """Simulate ``recv_into`` for Python 3.""" + if not self.messages: + return 0 + msg = self.messages.pop(0) + for index, byte in enumerate(msg): + buf[index] = byte + return len(msg) + + def recv(self, size): + """Simulate ``recv`` for Python 2.""" + try: + return self.messages.pop(0) + except IndexError: + return '' + + def send(self, val): + """Simulate a send.""" + return len(val) + + +def test_bytes_read(): + """Reader should capture bytes read.""" + sock = MockSocket() + sock.messages.append(b'foo') + rfile = makefile.MakeFile(sock, 'r') + rfile.read() + assert rfile.bytes_read == 3 + + +def test_bytes_written(): + """Writer should capture bytes written.""" + sock = MockSocket() + sock.messages.append(b'foo') + wfile = makefile.MakeFile(sock, 'w') + wfile.write(b'bar') + assert wfile.bytes_written == 3 diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_server.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_server.py new file mode 100644 index 000000000..e2e5f788c --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_server.py @@ -0,0 +1,558 @@ +"""Tests for the HTTP server.""" + +import os +import queue +import socket +import tempfile +import threading +import types +import uuid +import urllib.parse # noqa: WPS301 + +import pytest +import requests +import requests_unixsocket + +from pypytools.gc.custom import DefaultGc + +from .._compat import bton, ntob +from .._compat import IS_LINUX, IS_MACOS, IS_WINDOWS, SYS_PLATFORM +from ..server import IS_UID_GID_RESOLVABLE, Gateway, HTTPServer +from ..workers.threadpool import ThreadPool +from ..testing import ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + EPHEMERAL_PORT, +) + + +IS_SLOW_ENV = IS_MACOS or IS_WINDOWS + + +unix_only_sock_test = pytest.mark.skipif( + not hasattr(socket, 'AF_UNIX'), + reason='UNIX domain sockets are only available under UNIX-based OS', +) + + +non_macos_sock_test = pytest.mark.skipif( + IS_MACOS, + reason='Peercreds lookup does not work under macOS/BSD currently.', +) + + +@pytest.fixture(params=('abstract', 'file')) +def unix_sock_file(request): + """Check that bound UNIX socket address is stored in server.""" + name = 'unix_{request.param}_sock'.format(**locals()) + return request.getfixturevalue(name) + + +@pytest.fixture +def unix_abstract_sock(): + """Return an abstract UNIX socket address.""" + if not IS_LINUX: + pytest.skip( + '{os} does not support an abstract ' + 'socket namespace'.format(os=SYS_PLATFORM), + ) + return b''.join(( + b'\x00cheroot-test-socket', + ntob(str(uuid.uuid4())), + )).decode() + + +@pytest.fixture +def unix_file_sock(): + """Yield a unix file socket.""" + tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp() + + yield tmp_sock_fname + + os.close(tmp_sock_fh) + os.unlink(tmp_sock_fname) + + +def test_prepare_makes_server_ready(): + """Check that prepare() makes the server ready, and stop() clears it.""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + assert not httpserver.ready + assert not httpserver.requests._threads + + httpserver.prepare() + + assert httpserver.ready + assert httpserver.requests._threads + for thr in httpserver.requests._threads: + assert thr.ready + + httpserver.stop() + + assert not httpserver.requests._threads + assert not httpserver.ready + + +def test_stop_interrupts_serve(): + """Check that stop() interrupts running of serve().""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + httpserver.prepare() + serve_thread = threading.Thread(target=httpserver.serve) + serve_thread.start() + + serve_thread.join(0.5) + assert serve_thread.is_alive() + + httpserver.stop() + + serve_thread.join(0.5) + assert not serve_thread.is_alive() + + +@pytest.mark.parametrize( + 'exc_cls', + ( + IOError, + KeyboardInterrupt, + OSError, + RuntimeError, + ), +) +def test_server_interrupt(exc_cls): + """Check that assigning interrupt stops the server.""" + interrupt_msg = 'should catch {uuid!s}'.format(uuid=uuid.uuid4()) + raise_marker_sentinel = object() + + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + result_q = queue.Queue() + + def serve_thread(): + # ensure we catch the exception on the serve() thread + try: + httpserver.serve() + except exc_cls as e: + if str(e) == interrupt_msg: + result_q.put(raise_marker_sentinel) + + httpserver.prepare() + serve_thread = threading.Thread(target=serve_thread) + serve_thread.start() + + serve_thread.join(0.5) + assert serve_thread.is_alive() + + # this exception is raised on the serve() thread, + # not in the calling context. + httpserver.interrupt = exc_cls(interrupt_msg) + + serve_thread.join(0.5) + assert not serve_thread.is_alive() + assert result_q.get_nowait() is raise_marker_sentinel + + +def test_serving_is_false_and_stop_returns_after_ctrlc(): + """Check that stop() interrupts running of serve().""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + httpserver.prepare() + + # Simulate a Ctrl-C on the first call to `run`. + def raise_keyboard_interrupt(*args, **kwargs): + raise KeyboardInterrupt() + + httpserver._connections._selector.select = raise_keyboard_interrupt + + serve_thread = threading.Thread(target=httpserver.serve) + serve_thread.start() + + # The thread should exit right away due to the interrupt. + serve_thread.join( + httpserver.expiration_interval * (4 if IS_SLOW_ENV else 2), + ) + assert not serve_thread.is_alive() + + assert not httpserver._connections._serving + httpserver.stop() + + +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + ), +) +def test_bind_addr_inet(http_server, ip_addr): + """Check that bound IP address is stored in server.""" + httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) + + assert httpserver.bind_addr[0] == ip_addr + assert httpserver.bind_addr[1] != EPHEMERAL_PORT + + +@unix_only_sock_test +def test_bind_addr_unix(http_server, unix_sock_file): + """Check that bound UNIX socket address is stored in server.""" + httpserver = http_server.send(unix_sock_file) + + assert httpserver.bind_addr == unix_sock_file + + +@unix_only_sock_test +def test_bind_addr_unix_abstract(http_server, unix_abstract_sock): + """Check that bound UNIX abstract socket address is stored in server.""" + httpserver = http_server.send(unix_abstract_sock) + + assert httpserver.bind_addr == unix_abstract_sock + + +PEERCRED_IDS_URI = '/peer_creds/ids' +PEERCRED_TEXTS_URI = '/peer_creds/texts' + + +class _TestGateway(Gateway): + def respond(self): + req = self.req + conn = req.conn + req_uri = bton(req.uri) + if req_uri == PEERCRED_IDS_URI: + peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid + self.send_payload('|'.join(map(str, peer_creds))) + return + elif req_uri == PEERCRED_TEXTS_URI: + self.send_payload('!'.join((conn.peer_user, conn.peer_group))) + return + return super(_TestGateway, self).respond() + + def send_payload(self, payload): + req = self.req + req.status = b'200 OK' + req.ensure_headers_sent() + req.write(ntob(payload)) + + +@pytest.fixture +def peercreds_enabled_server(http_server, unix_sock_file): + """Construct a test server with ``peercreds_enabled``.""" + httpserver = http_server.send(unix_sock_file) + httpserver.gateway = _TestGateway + httpserver.peercreds_enabled = True + return httpserver + + +@unix_only_sock_test +@non_macos_sock_test +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_peercreds_unix_sock(http_request_timeout, peercreds_enabled_server): + """Check that ``PEERCRED`` lookup works when enabled.""" + httpserver = peercreds_enabled_server + bind_addr = httpserver.bind_addr + + if isinstance(bind_addr, bytes): + bind_addr = bind_addr.decode() + + # pylint: disable=possibly-unused-variable + quoted = urllib.parse.quote(bind_addr, safe='') + unix_base_uri = 'http+unix://{quoted}'.format(**locals()) + + expected_peercreds = os.getpid(), os.getuid(), os.getgid() + expected_peercreds = '|'.join(map(str, expected_peercreds)) + + with requests_unixsocket.monkeypatch(): + peercreds_resp = requests.get( + unix_base_uri + PEERCRED_IDS_URI, + timeout=http_request_timeout, + ) + peercreds_resp.raise_for_status() + assert peercreds_resp.text == expected_peercreds + + peercreds_text_resp = requests.get( + unix_base_uri + PEERCRED_TEXTS_URI, + timeout=http_request_timeout, + ) + assert peercreds_text_resp.status_code == 500 + + +@pytest.mark.skipif( + not IS_UID_GID_RESOLVABLE, + reason='Modules `grp` and `pwd` are not available ' + 'under the current platform', +) +@unix_only_sock_test +@non_macos_sock_test +def test_peercreds_unix_sock_with_lookup( + http_request_timeout, + peercreds_enabled_server, +): + """Check that ``PEERCRED`` resolution works when enabled.""" + httpserver = peercreds_enabled_server + httpserver.peercreds_resolve_enabled = True + + bind_addr = httpserver.bind_addr + + if isinstance(bind_addr, bytes): + bind_addr = bind_addr.decode() + + # pylint: disable=possibly-unused-variable + quoted = urllib.parse.quote(bind_addr, safe='') + unix_base_uri = 'http+unix://{quoted}'.format(**locals()) + + import grp + import pwd + expected_textcreds = ( + pwd.getpwuid(os.getuid()).pw_name, + grp.getgrgid(os.getgid()).gr_name, + ) + expected_textcreds = '!'.join(map(str, expected_textcreds)) + with requests_unixsocket.monkeypatch(): + peercreds_text_resp = requests.get( + unix_base_uri + PEERCRED_TEXTS_URI, + timeout=http_request_timeout, + ) + peercreds_text_resp.raise_for_status() + assert peercreds_text_resp.text == expected_textcreds + + +@pytest.mark.skipif( + IS_WINDOWS, + reason='This regression test is for a Linux bug, ' + 'and the resource module is not available on Windows', +) +@pytest.mark.parametrize( + 'resource_limit', + ( + 1024, + 2048, + ), + indirect=('resource_limit',), +) +@pytest.mark.usefixtures('many_open_sockets') +def test_high_number_of_file_descriptors(native_server_client, resource_limit): + """Test the server does not crash with a high file-descriptor value. + + This test shouldn't cause a server crash when trying to access + file-descriptor higher than 1024. + + The earlier implementation used to rely on ``select()`` syscall that + doesn't support file descriptors with numbers higher than 1024. + """ + # We want to force the server to use a file-descriptor with + # a number above resource_limit + + # Patch the method that processes + _old_process_conn = native_server_client.server_instance.process_conn + + def native_process_conn(conn): + native_process_conn.filenos.add(conn.socket.fileno()) + return _old_process_conn(conn) + native_process_conn.filenos = set() + native_server_client.server_instance.process_conn = native_process_conn + + # Trigger a crash if select() is used in the implementation + native_server_client.connect('/') + + # Ensure that at least one connection got accepted, otherwise the + # follow-up check wouldn't make sense + assert len(native_process_conn.filenos) > 0 + + # Check at least one of the sockets created are above the target number + assert any(fn >= resource_limit for fn in native_process_conn.filenos) + + +@pytest.mark.skipif( + not hasattr(socket, 'SO_REUSEPORT'), + reason='socket.SO_REUSEPORT is not supported on this platform', +) +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + ), +) +def test_reuse_port(http_server, ip_addr, mocker): + """Check that port initialized externally can be reused.""" + family = socket.getaddrinfo(ip_addr, EPHEMERAL_PORT)[0][0] + s = socket.socket(family) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s.bind((ip_addr, EPHEMERAL_PORT)) + server = HTTPServer( + bind_addr=s.getsockname()[:2], gateway=Gateway, reuse_port=True, + ) + spy = mocker.spy(server, 'prepare') + server.prepare() + server.stop() + s.close() + assert spy.spy_exception is None + + +ISSUE511 = IS_MACOS + + +if not IS_WINDOWS and not ISSUE511: + test_high_number_of_file_descriptors = pytest.mark.forked( + test_high_number_of_file_descriptors, + ) + + +@pytest.fixture +def _garbage_bin(): + """Disable garbage collection when this fixture is in use.""" + with DefaultGc().nogc(): + yield + + +@pytest.fixture +def resource_limit(request): + """Set the resource limit two times bigger then requested.""" + resource = pytest.importorskip( + 'resource', + reason='The "resource" module is Unix-specific', + ) + + # Get current resource limits to restore them later + soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) + + # We have to increase the nofile limit above 1024 + # Otherwise we see a 'Too many files open' error, instead of + # an error due to the file descriptor number being too high + resource.setrlimit( + resource.RLIMIT_NOFILE, + (request.param * 2, hard_limit), + ) + + try: # noqa: WPS501 + yield request.param + finally: + # Reset the resource limit back to the original soft limit + resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, hard_limit)) + + +@pytest.fixture +def many_open_sockets(request, resource_limit): + """Allocate a lot of file descriptors by opening dummy sockets.""" + # NOTE: `@pytest.mark.usefixtures` doesn't work on fixtures which + # NOTE: forces us to invoke this one dynamically to avoid having an + # NOTE: unused argument. + request.getfixturevalue('_garbage_bin') + + # Hoard a lot of file descriptors by opening and storing a lot of sockets + test_sockets = [] + # Open a lot of file descriptors, so the next one the server + # opens is a high number + try: + for _ in range(resource_limit): + sock = socket.socket() + test_sockets.append(sock) + # If we reach a high enough number, we don't need to open more + if sock.fileno() >= resource_limit: + break + # Check we opened enough descriptors to reach a high number + the_highest_fileno = test_sockets[-1].fileno() + assert the_highest_fileno >= resource_limit + yield the_highest_fileno + finally: + # Close our open resources + for test_socket in test_sockets: + test_socket.close() + + +@pytest.mark.parametrize( + ('minthreads', 'maxthreads', 'inited_maxthreads'), + ( + ( + # NOTE: The docstring only mentions -1 to mean "no max", but other + # NOTE: negative numbers should also work. + 1, + -2, + float('inf'), + ), + (1, -1, float('inf')), + (1, 1, 1), + (1, 2, 2), + (1, float('inf'), float('inf')), + (2, -2, float('inf')), + (2, -1, float('inf')), + (2, 2, 2), + (2, float('inf'), float('inf')), + ), +) +def test_threadpool_threadrange_set(minthreads, maxthreads, inited_maxthreads): + """Test setting the number of threads in a ThreadPool. + + The ThreadPool should properly set the min+max number of the threads to use + in the pool if those limits are valid. + """ + tp = ThreadPool( + server=None, + min=minthreads, + max=maxthreads, + ) + assert tp.min == minthreads + assert tp.max == inited_maxthreads + + +@pytest.mark.parametrize( + ('minthreads', 'maxthreads', 'error'), + ( + (-1, -1, 'min=-1 must be > 0'), + (-1, 0, 'min=-1 must be > 0'), + (-1, 1, 'min=-1 must be > 0'), + (-1, 2, 'min=-1 must be > 0'), + (0, -1, 'min=0 must be > 0'), + (0, 0, 'min=0 must be > 0'), + (0, 1, 'min=0 must be > 0'), + (0, 2, 'min=0 must be > 0'), + (1, 0, 'Expected an integer or the infinity value for the `max` argument but got 0.'), + (1, 0.5, 'Expected an integer or the infinity value for the `max` argument but got 0.5.'), + (2, 0, 'Expected an integer or the infinity value for the `max` argument but got 0.'), + (2, '1', "Expected an integer or the infinity value for the `max` argument but got '1'."), + (2, 1, 'max=1 must be > min=2'), + ), +) +def test_threadpool_invalid_threadrange(minthreads, maxthreads, error): + """Test that a ThreadPool rejects invalid min/max values. + + The ThreadPool should raise an error with the proper message when + initialized with an invalid min+max number of threads. + """ + with pytest.raises((ValueError, TypeError), match=error): + ThreadPool( + server=None, + min=minthreads, + max=maxthreads, + ) + + +def test_threadpool_multistart_validation(monkeypatch): + """Test for ThreadPool multi-start behavior. + + Tests that when calling start() on a ThreadPool multiple times raises a + :exc:`RuntimeError` + """ + # replace _spawn_worker with a function that returns a placeholder to avoid + # actually starting any threads + monkeypatch.setattr( + ThreadPool, + '_spawn_worker', + lambda _: types.SimpleNamespace(ready=True), + ) + + tp = ThreadPool(server=None) + tp.start() + with pytest.raises(RuntimeError, match='Threadpools can only be started once.'): + tp.start() diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_ssl.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_ssl.py new file mode 100644 index 000000000..1900e20d1 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_ssl.py @@ -0,0 +1,702 @@ +"""Tests for TLS support.""" + +import functools +import json +import os +import ssl +import subprocess +import sys +import threading +import time +import traceback +import http.client + +import OpenSSL.SSL +import pytest +import requests +import trustme + +from .._compat import bton, ntob, ntou +from .._compat import IS_ABOVE_OPENSSL10, IS_CI, IS_PYPY +from .._compat import IS_LINUX, IS_MACOS, IS_WINDOWS +from ..server import HTTPServer, get_ssl_adapter_class +from ..testing import ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + EPHEMERAL_PORT, + # get_server_client, + _get_conn_data, + _probe_ipv6_sock, +) +from ..wsgi import Gateway_10 + + +IS_GITHUB_ACTIONS_WORKFLOW = bool(os.getenv('GITHUB_WORKFLOW')) +IS_WIN2016 = ( + IS_WINDOWS + # pylint: disable=unsupported-membership-test + and b'Microsoft Windows Server 2016 Datacenter' in subprocess.check_output( + ('systeminfo',), + ) +) +IS_LIBRESSL_BACKEND = ssl.OPENSSL_VERSION.startswith('LibreSSL') +IS_PYOPENSSL_SSL_VERSION_1_0 = ( + OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION). + startswith(b'OpenSSL 1.0.') +) +PY310_PLUS = sys.version_info[:2] >= (3, 10) + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: + OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} + + +missing_ipv6 = pytest.mark.skipif( + not _probe_ipv6_sock('::1'), + reason='' + 'IPv6 is disabled ' + '(for example, under Travis CI ' + 'which runs under GCE supporting only IPv4)', +) + + +class HelloWorldGateway(Gateway_10): + """Gateway responding with Hello World to root URI.""" + + def respond(self): + """Respond with dummy content via HTTP.""" + req = self.req + req_uri = bton(req.uri) + if req_uri == '/': + req.status = b'200 OK' + req.ensure_headers_sent() + req.write(b'Hello world!') + return + if req_uri == '/env': + req.status = b'200 OK' + req.ensure_headers_sent() + env = self.get_environ() + # drop files so that it can be json dumped + env.pop('wsgi.errors') + env.pop('wsgi.input') + print(env) + req.write(json.dumps(env).encode('utf-8')) + return + return super(HelloWorldGateway, self).respond() + + +def make_tls_http_server(bind_addr, ssl_adapter, request): + """Create and start an HTTP server bound to ``bind_addr``.""" + httpserver = HTTPServer( + bind_addr=bind_addr, + gateway=HelloWorldGateway, + ) + # httpserver.gateway = HelloWorldGateway + httpserver.ssl_adapter = ssl_adapter + + threading.Thread(target=httpserver.safe_start).start() + + while not httpserver.ready: + time.sleep(0.1) + + request.addfinalizer(httpserver.stop) + + return httpserver + + +@pytest.fixture +def tls_http_server(request): + """Provision a server creator as a fixture.""" + return functools.partial(make_tls_http_server, request=request) + + +@pytest.fixture +def ca(): + """Provide a certificate authority via fixture.""" + return trustme.CA() + + +@pytest.fixture +def tls_ca_certificate_pem_path(ca): + """Provide a certificate authority certificate file via fixture.""" + with ca.cert_pem.tempfile() as ca_cert_pem: + yield ca_cert_pem + + +@pytest.fixture +def tls_certificate(ca): + """Provide a leaf certificate via fixture.""" + interface, _host, _port = _get_conn_data(ANY_INTERFACE_IPV4) + return ca.issue_cert(ntou(interface)) + + +@pytest.fixture +def tls_certificate_chain_pem_path(tls_certificate): + """Provide a certificate chain PEM file path via fixture.""" + with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem: + yield cert_pem + + +@pytest.fixture +def tls_certificate_private_key_pem_path(tls_certificate): + """Provide a certificate private key PEM file path via fixture.""" + with tls_certificate.private_key_pem.tempfile() as cert_key_pem: + yield cert_key_pem + + +def _thread_except_hook(exceptions, args): + """Append uncaught exception ``args`` in threads to ``exceptions``.""" + if issubclass(args.exc_type, SystemExit): + return + # cannot store the exception, it references the thread's stack + exceptions.append(( + args.exc_type, + str(args.exc_value), + ''.join( + traceback.format_exception( + args.exc_type, args.exc_value, args.exc_traceback, + ), + ), + )) + + +@pytest.fixture +def thread_exceptions(): + """Provide a list of uncaught exceptions from threads via a fixture. + + Only catches exceptions on Python 3.8+. + The list contains: ``(type, str(value), str(traceback))`` + """ + exceptions = [] + # Python 3.8+ + orig_hook = getattr(threading, 'excepthook', None) + if orig_hook is not None: + threading.excepthook = functools.partial( + _thread_except_hook, exceptions, + ) + try: + yield exceptions + finally: + if orig_hook is not None: + threading.excepthook = orig_hook + + +@pytest.mark.parametrize( + 'adapter_type', + ( + 'builtin', + 'pyopenssl', + ), +) +def test_ssl_adapters( + http_request_timeout, + tls_http_server, adapter_type, + tls_certificate, + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, + tls_ca_certificate_pem_path, +): + """Test ability to connect to server via HTTPS using adapters.""" + interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4) + tls_adapter_cls = get_ssl_adapter_class(name=adapter_type) + tls_adapter = tls_adapter_cls( + tls_certificate_chain_pem_path, tls_certificate_private_key_pem_path, + ) + if adapter_type == 'pyopenssl': + tls_adapter.context = tls_adapter.get_context() + + tls_certificate.configure_cert(tls_adapter.context) + + tlshttpserver = tls_http_server((interface, port), tls_adapter) + + # testclient = get_server_client(tlshttpserver) + # testclient.get('/') + + interface, _host, port = _get_conn_data( + tlshttpserver.bind_addr, + ) + + resp = requests.get( + 'https://{host!s}:{port!s}/'.format(host=interface, port=port), + timeout=http_request_timeout, + verify=tls_ca_certificate_pem_path, + ) + + assert resp.status_code == 200 + assert resp.text == 'Hello world!' + + +@pytest.mark.parametrize( # noqa: C901 # FIXME + 'adapter_type', + ( + 'builtin', + 'pyopenssl', + ), +) +@pytest.mark.parametrize( + ('is_trusted_cert', 'tls_client_identity'), + ( + (True, 'localhost'), (True, '127.0.0.1'), + (True, '*.localhost'), (True, 'not_localhost'), + (False, 'localhost'), + ), +) +@pytest.mark.parametrize( + 'tls_verify_mode', + ( + ssl.CERT_NONE, # server shouldn't validate client cert + ssl.CERT_OPTIONAL, # same as CERT_REQUIRED in client mode, don't use + ssl.CERT_REQUIRED, # server should validate if client cert CA is OK + ), +) +@pytest.mark.xfail( + IS_PYPY and IS_CI, + reason='Fails under PyPy in CI for unknown reason', + strict=False, +) +def test_tls_client_auth( # noqa: C901, WPS213 # FIXME + # FIXME: remove twisted logic, separate tests + http_request_timeout, + mocker, + tls_http_server, adapter_type, + ca, + tls_certificate, + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, + tls_ca_certificate_pem_path, + is_trusted_cert, tls_client_identity, + tls_verify_mode, +): + """Verify that client TLS certificate auth works correctly.""" + test_cert_rejection = ( + tls_verify_mode != ssl.CERT_NONE + and not is_trusted_cert + ) + interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4) + + client_cert_root_ca = ca if is_trusted_cert else trustme.CA() + with mocker.mock_module.patch( + 'idna.core.ulabel', + return_value=ntob(tls_client_identity), + ): + client_cert = client_cert_root_ca.issue_cert( + ntou(tls_client_identity), + ) + del client_cert_root_ca + + with client_cert.private_key_and_cert_chain_pem.tempfile() as cl_pem: + tls_adapter_cls = get_ssl_adapter_class(name=adapter_type) + tls_adapter = tls_adapter_cls( + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, + ) + if adapter_type == 'pyopenssl': + tls_adapter.context = tls_adapter.get_context() + tls_adapter.context.set_verify( + _stdlib_to_openssl_verify[tls_verify_mode], + lambda conn, cert, errno, depth, preverify_ok: preverify_ok, + ) + else: + tls_adapter.context.verify_mode = tls_verify_mode + + ca.configure_trust(tls_adapter.context) + tls_certificate.configure_cert(tls_adapter.context) + + tlshttpserver = tls_http_server((interface, port), tls_adapter) + + interface, _host, port = _get_conn_data(tlshttpserver.bind_addr) + + make_https_request = functools.partial( + requests.get, + 'https://{host!s}:{port!s}/'.format(host=interface, port=port), + + # Don't wait for the first byte forever: + timeout=http_request_timeout, + + # Server TLS certificate verification: + verify=tls_ca_certificate_pem_path, + + # Client TLS certificate verification: + cert=cl_pem, + ) + + if not test_cert_rejection: + resp = make_https_request() + is_req_successful = resp.status_code == 200 + if ( + not is_req_successful + and IS_PYOPENSSL_SSL_VERSION_1_0 + and adapter_type == 'builtin' + and tls_verify_mode == ssl.CERT_REQUIRED + and tls_client_identity == 'localhost' + and is_trusted_cert + ): + pytest.xfail( + 'OpenSSL 1.0 has problems with verifying client certs', + ) + assert is_req_successful + assert resp.text == 'Hello world!' + resp.close() + return + + # xfail some flaky tests + # https://github.com/cherrypy/cheroot/issues/237 + issue_237 = ( + IS_MACOS + and adapter_type == 'builtin' + and tls_verify_mode != ssl.CERT_NONE + ) + if issue_237: + pytest.xfail('Test sometimes fails') + + expected_ssl_errors = requests.exceptions.SSLError, + if IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW: + expected_ssl_errors += requests.exceptions.ConnectionError, + with pytest.raises(expected_ssl_errors) as ssl_err: + make_https_request().close() + + try: + err_text = ssl_err.value.args[0].reason.args[0].args[0] + except AttributeError: + if IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW: + err_text = str(ssl_err.value) + else: + raise + + if isinstance(err_text, int): + err_text = str(ssl_err.value) + + expected_substrings = ( + 'sslv3 alert bad certificate' if IS_LIBRESSL_BACKEND + else 'tlsv1 alert unknown ca', + ) + if IS_MACOS and IS_PYPY and adapter_type == 'pyopenssl': + expected_substrings = ('tlsv1 alert unknown ca',) + if ( + tls_verify_mode in ( + ssl.CERT_REQUIRED, + ssl.CERT_OPTIONAL, + ) + and not is_trusted_cert + and tls_client_identity == 'localhost' + ): + expected_substrings += ( + 'bad handshake: ' + "SysCallError(10054, 'WSAECONNRESET')", + "('Connection aborted.', " + 'OSError("(10054, \'WSAECONNRESET\')"))', + "('Connection aborted.', " + 'OSError("(10054, \'WSAECONNRESET\')",))', + "('Connection aborted.', " + 'error("(10054, \'WSAECONNRESET\')",))', + "('Connection aborted.', " + 'ConnectionResetError(10054, ' + "'An existing connection was forcibly closed " + "by the remote host', None, 10054, None))", + "('Connection aborted.', " + 'error(10054, ' + "'An existing connection was forcibly closed " + "by the remote host'))", + ) if IS_WINDOWS else ( + "('Connection aborted.', " + 'OSError("(104, \'ECONNRESET\')"))', + "('Connection aborted.', " + 'OSError("(104, \'ECONNRESET\')",))', + "('Connection aborted.', " + 'error("(104, \'ECONNRESET\')",))', + "('Connection aborted.', " + "ConnectionResetError(104, 'Connection reset by peer'))", + "('Connection aborted.', " + "error(104, 'Connection reset by peer'))", + ) if ( + IS_GITHUB_ACTIONS_WORKFLOW + and IS_LINUX + ) else ( + "('Connection aborted.', " + "BrokenPipeError(32, 'Broken pipe'))", + ) + + if PY310_PLUS: + # FIXME: Figure out what's happening and correct the problem + expected_substrings += ( + 'SSLError(SSLEOFError(8, ' + "'EOF occurred in violation of protocol (_ssl.c:", + ) + if IS_GITHUB_ACTIONS_WORKFLOW and IS_WINDOWS and PY310_PLUS: + expected_substrings += ( + "('Connection aborted.', " + 'RemoteDisconnected(' + "'Remote end closed connection without response'))", + ) + + assert any(e in err_text for e in expected_substrings) + + +@pytest.mark.parametrize( # noqa: C901 # FIXME + 'adapter_type', + ( + pytest.param( + 'builtin', + marks=pytest.mark.xfail( + IS_MACOS and PY310_PLUS, + reason='Unclosed TLS resource warnings happen on macOS ' + 'under Python 3.10 (#508)', + strict=False, + ), + ), + 'pyopenssl', + ), +) +@pytest.mark.parametrize( + ('tls_verify_mode', 'use_client_cert'), + ( + (ssl.CERT_NONE, False), + (ssl.CERT_NONE, True), + (ssl.CERT_OPTIONAL, False), + (ssl.CERT_OPTIONAL, True), + (ssl.CERT_REQUIRED, True), + ), +) +def test_ssl_env( # noqa: C901 # FIXME + thread_exceptions, + recwarn, + mocker, + http_request_timeout, + tls_http_server, adapter_type, + ca, tls_verify_mode, tls_certificate, + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, + tls_ca_certificate_pem_path, + use_client_cert, +): + """Test the SSL environment generated by the SSL adapters.""" + interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4) + + with mocker.mock_module.patch( + 'idna.core.ulabel', + return_value=ntob('127.0.0.1'), + ): + client_cert = ca.issue_cert(ntou('127.0.0.1')) + + with client_cert.private_key_and_cert_chain_pem.tempfile() as cl_pem: + tls_adapter_cls = get_ssl_adapter_class(name=adapter_type) + tls_adapter = tls_adapter_cls( + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, + ) + if adapter_type == 'pyopenssl': + tls_adapter.context = tls_adapter.get_context() + tls_adapter.context.set_verify( + _stdlib_to_openssl_verify[tls_verify_mode], + lambda conn, cert, errno, depth, preverify_ok: preverify_ok, + ) + else: + tls_adapter.context.verify_mode = tls_verify_mode + + ca.configure_trust(tls_adapter.context) + tls_certificate.configure_cert(tls_adapter.context) + + tlswsgiserver = tls_http_server((interface, port), tls_adapter) + + interface, _host, port = _get_conn_data(tlswsgiserver.bind_addr) + + resp = requests.get( + 'https://' + interface + ':' + str(port) + '/env', + timeout=http_request_timeout, + verify=tls_ca_certificate_pem_path, + cert=cl_pem if use_client_cert else None, + ) + + env = json.loads(resp.content.decode('utf-8')) + + # hard coded env + assert env['wsgi.url_scheme'] == 'https' + assert env['HTTPS'] == 'on' + + # ensure these are present + for key in {'SSL_VERSION_INTERFACE', 'SSL_VERSION_LIBRARY'}: + assert key in env + + # pyOpenSSL generates the env before the handshake completes + if adapter_type == 'pyopenssl': + return + + for key in {'SSL_PROTOCOL', 'SSL_CIPHER'}: + assert key in env + + # client certificate env + if tls_verify_mode == ssl.CERT_NONE or not use_client_cert: + assert env['SSL_CLIENT_VERIFY'] == 'NONE' + else: + assert env['SSL_CLIENT_VERIFY'] == 'SUCCESS' + + with open(cl_pem, 'rt') as f: + assert env['SSL_CLIENT_CERT'] in f.read() + + for key in { + 'SSL_CLIENT_M_VERSION', 'SSL_CLIENT_M_SERIAL', + 'SSL_CLIENT_I_DN', 'SSL_CLIENT_S_DN', + }: + assert key in env + + # builtin ssl environment generation may use a loopback socket + # ensure no ResourceWarning was raised during the test + if IS_PYPY: + # NOTE: PyPy doesn't have ResourceWarning + # Ref: https://doc.pypy.org/en/latest/cpython_differences.html + return + for warn in recwarn: + if not issubclass(warn.category, ResourceWarning): + continue + + # the tests can sporadically generate resource warnings + # due to timing issues + # all of these sporadic warnings appear to be about socket.socket + # and have been observed to come from requests connection pool + msg = str(warn.message) + if 'socket.socket' in msg: + pytest.xfail( + '\n'.join(( + 'Sometimes this test fails due to ' + 'a socket.socket ResourceWarning:', + msg, + )), + ) + pytest.fail(msg) + + # to perform the ssl handshake over that loopback socket, + # the builtin ssl environment generation uses a thread + for _, _, trace in thread_exceptions: + print(trace, file=sys.stderr) + assert not thread_exceptions, ': '.join(( + thread_exceptions[0][0].__name__, + thread_exceptions[0][1], + )) + + +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + ), +) +def test_https_over_http_error(http_server, ip_addr): + """Ensure that connecting over HTTPS to HTTP port is handled.""" + httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) + interface, _host, port = _get_conn_data(httpserver.bind_addr) + with pytest.raises(ssl.SSLError) as ssl_err: + http.client.HTTPSConnection( + '{interface}:{port}'.format( + interface=interface, + port=port, + ), + ).request('GET', '/') + expected_substring = ( + 'wrong version number' if IS_ABOVE_OPENSSL10 + else 'unknown protocol' + ) + assert expected_substring in ssl_err.value.args[-1] + + +@pytest.mark.parametrize( + 'adapter_type', + ( + 'builtin', + 'pyopenssl', + ), +) +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + pytest.param(ANY_INTERFACE_IPV6, marks=missing_ipv6), + ), +) +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_http_over_https_error( + http_request_timeout, + tls_http_server, adapter_type, + ca, ip_addr, + tls_certificate, + tls_certificate_chain_pem_path, + tls_certificate_private_key_pem_path, +): + """Ensure that connecting over HTTP to HTTPS port is handled.""" + # disable some flaky tests + # https://github.com/cherrypy/cheroot/issues/225 + issue_225 = ( + IS_MACOS + and adapter_type == 'builtin' + ) + if issue_225: + pytest.xfail('Test fails in Travis-CI') + + tls_adapter_cls = get_ssl_adapter_class(name=adapter_type) + tls_adapter = tls_adapter_cls( + tls_certificate_chain_pem_path, tls_certificate_private_key_pem_path, + ) + if adapter_type == 'pyopenssl': + tls_adapter.context = tls_adapter.get_context() + + tls_certificate.configure_cert(tls_adapter.context) + + interface, _host, port = _get_conn_data(ip_addr) + tlshttpserver = tls_http_server((interface, port), tls_adapter) + + interface, _host, port = _get_conn_data( + tlshttpserver.bind_addr, + ) + + fqdn = interface + if ip_addr is ANY_INTERFACE_IPV6: + fqdn = '[{fqdn}]'.format(**locals()) + + expect_fallback_response_over_plain_http = ( + ( + adapter_type == 'pyopenssl' + ) + ) + if expect_fallback_response_over_plain_http: + resp = requests.get( + 'http://{host!s}:{port!s}/'.format(host=fqdn, port=port), + timeout=http_request_timeout, + ) + assert resp.status_code == 400 + assert resp.text == ( + 'The client sent a plain HTTP request, ' + 'but this server only speaks HTTPS on this port.' + ) + return + + with pytest.raises(requests.exceptions.ConnectionError) as ssl_err: + requests.get( # FIXME: make stdlib ssl behave like PyOpenSSL + 'http://{host!s}:{port!s}/'.format(host=fqdn, port=port), + timeout=http_request_timeout, + ) + + if IS_LINUX: + expected_error_code, expected_error_text = ( + 104, 'Connection reset by peer', + ) + if IS_MACOS: + expected_error_code, expected_error_text = ( + 54, 'Connection reset by peer', + ) + if IS_WINDOWS: + expected_error_code, expected_error_text = ( + 10054, + 'An existing connection was forcibly closed by the remote host', + ) + + underlying_error = ssl_err.value.args[0].args[-1] + err_text = str(underlying_error) + assert underlying_error.errno == expected_error_code, ( + 'The underlying error is {underlying_error!r}'. + format(**locals()) + ) + assert expected_error_text in err_text diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_wsgi.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_wsgi.py new file mode 100644 index 000000000..14005a84e --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/test_wsgi.py @@ -0,0 +1,85 @@ +"""Test wsgi.""" + +from concurrent.futures.thread import ThreadPoolExecutor +from traceback import print_tb + +import pytest +import portend +import requests +from requests_toolbelt.sessions import BaseUrlSession as Session +from jaraco.context import ExceptionTrap + +from cheroot import wsgi +from cheroot._compat import IS_MACOS, IS_WINDOWS + + +IS_SLOW_ENV = IS_MACOS or IS_WINDOWS + + +@pytest.fixture +def simple_wsgi_server(): + """Fucking simple wsgi server fixture (duh).""" + port = portend.find_available_local_port() + + def app(_environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + return [b'Hello world!'] + + host = '::' + addr = host, port + server = wsgi.Server(addr, app, timeout=600 if IS_SLOW_ENV else 20) + # pylint: disable=possibly-unused-variable + url = 'http://localhost:{port}/'.format(**locals()) + # pylint: disable=possibly-unused-variable + with server._run_in_thread() as thread: + yield locals() + + +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_connection_keepalive(simple_wsgi_server): + """Test the connection keepalive works (duh).""" + session = Session(base_url=simple_wsgi_server['url']) + pooled = requests.adapters.HTTPAdapter( + pool_connections=1, pool_maxsize=1000, + ) + session.mount('http://', pooled) + + def do_request(): + with ExceptionTrap(requests.exceptions.ConnectionError) as trap: + resp = session.get('info') + resp.raise_for_status() + print_tb(trap.tb) + return bool(trap) + + with ThreadPoolExecutor(max_workers=10 if IS_SLOW_ENV else 50) as pool: + tasks = [ + pool.submit(do_request) + for n in range(250 if IS_SLOW_ENV else 1000) + ] + failures = sum(task.result() for task in tasks) + + session.close() + assert not failures + + +def test_gateway_start_response_called_twice(monkeypatch): + """Verify that repeat calls of ``Gateway.start_response()`` fail.""" + monkeypatch.setattr(wsgi.Gateway, 'get_environ', lambda self: {}) + wsgi_gateway = wsgi.Gateway(None) + wsgi_gateway.started_response = True + + err_msg = '^WSGI start_response called a second time with no exc_info.$' + with pytest.raises(RuntimeError, match=err_msg): + wsgi_gateway.start_response('200', (), None) + + +def test_gateway_write_needs_start_response_called_before(monkeypatch): + """Check that calling ``Gateway.write()`` needs started response.""" + monkeypatch.setattr(wsgi.Gateway, 'get_environ', lambda self: {}) + wsgi_gateway = wsgi.Gateway(None) + + err_msg = '^WSGI write called before start_response.$' + with pytest.raises(RuntimeError, match=err_msg): + wsgi_gateway.write(None) # The actual arg value is unimportant diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/test/webtest.py b/awesome_venv/lib/python3.10/site-packages/cheroot/test/webtest.py new file mode 100644 index 000000000..eafa2dd6c --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/test/webtest.py @@ -0,0 +1,588 @@ +"""Extensions to unittest for web frameworks. + +Use the :py:meth:`WebCase.getPage` method to request a page +from your HTTP server. + +Framework Integration +===================== +If you have control over your server process, you can handle errors +in the server-side of the HTTP conversation a bit better. You must run +both the client (your :py:class:`WebCase` tests) and the server in the +same process (but in separate threads, obviously). +When an error occurs in the framework, call server_error. It will print +the traceback to stdout, and keep any assertions you have from running +(the assumption is that, if the server errors, the page output will not +be of further significance to your tests). +""" + +import pprint +import re +import socket +import sys +import time +import traceback +import os +import json +import unittest # pylint: disable=deprecated-module,preferred-module +import warnings +import functools +import http.client +import urllib.parse + +from more_itertools.more import always_iterable +import jaraco.functools + + +def interface(host): + """Return an IP address for a client connection given the server host. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + if host == '0.0.0.0': + # INADDR_ANY, which should respond on localhost. + return '127.0.0.1' + if host == '::': + # IN6ADDR_ANY, which should respond on localhost. + return '::1' + return host + + +try: + # Jython support + if sys.platform[:4] == 'java': + def getchar(): + """Get a key press.""" + # Hopefully this is enough + return sys.stdin.read(1) + else: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + """Get a key press.""" + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + """Get a key press.""" + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +# from jaraco.properties +class NonDataProperty: + """Non-data property decorator.""" + + def __init__(self, fget): + """Initialize a non-data property.""" + assert fget is not None, 'fget cannot be none' + assert callable(fget), 'fget must be callable' + self.fget = fget + + def __get__(self, obj, objtype=None): + """Return a class property.""" + if obj is None: + return self + return self.fget(obj) + + +class WebCase(unittest.TestCase): + """Helper web test suite base.""" + + HOST = '127.0.0.1' + PORT = 8000 + HTTP_CONN = http.client.HTTPConnection + PROTOCOL = 'HTTP/1.1' + + scheme = 'http' + url = None + ssl_context = None + + status = None + headers = None + body = None + + encoding = 'utf-8' + + time = None + + @property + def _Conn(self): + """Return HTTPConnection or HTTPSConnection based on self.scheme. + + * from :py:mod:`python:http.client`. + """ + cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper()) + return getattr(http.client, cls_name) + + def get_conn(self, auto_open=False): + """Return a connection to our HTTP server.""" + conn = self._Conn(self.interface(), self.PORT) + # Automatically re-connect? + conn.auto_open = auto_open + conn.connect() + return conn + + def set_persistent(self, on=True, auto_open=False): + """Make our HTTP_CONN persistent (or not). + + If the 'on' argument is True (the default), then self.HTTP_CONN + will be set to an instance of HTTP(S)?Connection + to persist across requests. + As this class only allows for a single open connection, if + self already has an open connection, it will be closed. + """ + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + self.HTTP_CONN = ( + self.get_conn(auto_open=auto_open) + if on + else self._Conn + ) + + @property + def persistent(self): + """Presence of the persistent HTTP connection.""" + return hasattr(self.HTTP_CONN, '__class__') + + @persistent.setter + def persistent(self, on): + self.set_persistent(on) + + def interface(self): + """Return an IP address for a client connection. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + return interface(self.HOST) + + def getPage( + self, url, headers=None, method='GET', body=None, + protocol=None, raise_subcls=(), + ): + """Open the url with debugging support. + + Return status, headers, body. + + url should be the identifier passed to the server, typically a + server-absolute path and query string (sent between method and + protocol), and should only be an absolute URI if proxy support is + enabled in the server. + + If the application under test generates absolute URIs, be sure + to wrap them first with :py:func:`strip_netloc`:: + + >>> class MyAppWebCase(WebCase): + ... def getPage(url, *args, **kwargs): + ... super(MyAppWebCase, self).getPage( + ... cheroot.test.webtest.strip_netloc(url), + ... *args, **kwargs + ... ) + + ``raise_subcls`` is passed through to :py:func:`openURL`. + """ + ServerError.on = False + + if isinstance(url, str): + url = url.encode('utf-8') + if isinstance(body, str): + body = body.encode('utf-8') + + # for compatibility, support raise_subcls is None + raise_subcls = raise_subcls or () + + self.url = url + self.time = None + start = time.time() + result = openURL( + url, headers, method, body, self.HOST, self.PORT, + self.HTTP_CONN, protocol or self.PROTOCOL, + raise_subcls=raise_subcls, + ssl_context=self.ssl_context, + ) + self.time = time.time() - start + self.status, self.headers, self.body = result + + # Build a list of request cookies from the previous response cookies. + self.cookies = [ + ('Cookie', v) for k, v in self.headers + if k.lower() == 'set-cookie' + ] + + if ServerError.on: + raise ServerError() + return result + + @NonDataProperty + def interactive(self): + """Determine whether tests are run in interactive mode. + + Load interactivity setting from environment, where + the value can be numeric or a string like true or + False or 1 or 0. + """ + env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True') + is_interactive = bool(json.loads(env_str.lower())) + if is_interactive: + warnings.warn( + 'Interactive test failure interceptor support via ' + 'WEBTEST_INTERACTIVE environment variable is deprecated.', + DeprecationWarning, + ) + return is_interactive + + console_height = 30 + + def _handlewebError(self, msg): # noqa: C901 # FIXME + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = ( + ' Show: ' + '[B]ody [H]eaders [S]tatus [U]RL; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ' + ) + sys.stdout.write(p) + sys.stdout.flush() + while True: + i = getchar().upper() + if not isinstance(i, type('')): + i = i.decode('ascii') + if i not in 'BHSUIRX': + continue + print(i.upper()) # Also prints new line + if i == 'B': + for x, line in enumerate(self.body.splitlines()): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r') + if m == 'q': + break + print(line) + elif i == 'H': + pprint.pprint(self.headers) + elif i == 'S': + print(self.status) + elif i == 'U': + print(self.url) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise self.failureException(msg) + elif i == 'X': + sys.exit() + sys.stdout.write(p) + sys.stdout.flush() + + @property + def status_code(self): # noqa: D401; irrelevant for properties + """Integer HTTP status code.""" + return int(self.status[:3]) + + def status_matches(self, expected): + """Check whether actual status matches expected.""" + actual = ( + self.status_code + if isinstance(expected, int) else + self.status + ) + return expected == actual + + def assertStatus(self, status, msg=None): + """Fail if self.status != status. + + status may be integer code, exact string status, or + iterable of allowed possibilities. + """ + if any(map(self.status_matches, always_iterable(status))): + return + + tmpl = 'Status {self.status} does not match {status}' + msg = msg or tmpl.format(**locals()) + self._handlewebError(msg) + + def assertHeader(self, key, value=None, msg=None): + """Fail if (key, [value]) not in self.headers.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + if value is None or str(value) == v: + return v + + if msg is None: + if value is None: + msg = '%r not in headers' % key + else: + msg = '%r:%r not in headers' % (key, value) + self._handlewebError(msg) + + def assertHeaderIn(self, key, values, msg=None): + """Fail if header indicated by key doesn't have one of the values.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + matches = [value for value in values if str(value) == v] + if matches: + return matches + + if msg is None: + msg = '%(key)r not in %(values)r' % vars() + self._handlewebError(msg) + + def assertHeaderItemValue(self, key, value, msg=None): + """Fail if the header does not contain the specified value.""" + actual_value = self.assertHeader(key, msg=msg) + header_values = map(str.strip, actual_value.split(',')) + if value in header_values: + return value + + if msg is None: + msg = '%r not in %r' % (value, header_values) + self._handlewebError(msg) + + def assertNoHeader(self, key, msg=None): + """Fail if key in self.headers.""" + lowkey = key.lower() + matches = [k for k, v in self.headers if k.lower() == lowkey] + if matches: + if msg is None: + msg = '%r in headers' % key + self._handlewebError(msg) + + def assertNoHeaderItemValue(self, key, value, msg=None): + """Fail if the header contains the specified value.""" + lowkey = key.lower() + hdrs = self.headers + matches = [k for k, v in hdrs if k.lower() == lowkey and v == value] + if matches: + if msg is None: + msg = '%r:%r in %r' % (key, value, hdrs) + self._handlewebError(msg) + + def assertBody(self, value, msg=None): + """Fail if value != self.body.""" + if isinstance(value, str): + value = value.encode(self.encoding) + if value != self.body: + if msg is None: + msg = 'expected body:\n%r\n\nactual body:\n%r' % ( + value, self.body, + ) + self._handlewebError(msg) + + def assertInBody(self, value, msg=None): + """Fail if value not in self.body.""" + if isinstance(value, str): + value = value.encode(self.encoding) + if value not in self.body: + if msg is None: + msg = '%r not in body: %s' % (value, self.body) + self._handlewebError(msg) + + def assertNotInBody(self, value, msg=None): + """Fail if value in self.body.""" + if isinstance(value, str): + value = value.encode(self.encoding) + if value in self.body: + if msg is None: + msg = '%r found in body' % value + self._handlewebError(msg) + + def assertMatchesBody(self, pattern, msg=None, flags=0): + """Fail if value (a regex pattern) is not in self.body.""" + if isinstance(pattern, str): + pattern = pattern.encode(self.encoding) + if re.search(pattern, self.body, flags) is None: + if msg is None: + msg = 'No match for %r in body' % pattern + self._handlewebError(msg) + + +methods_with_bodies = ('POST', 'PUT', 'PATCH') + + +def cleanHeaders(headers, method, body, host, port): + """Return request headers, with required headers added (if missing).""" + if headers is None: + headers = [] + + # Add the required Host request header if not present. + # [This specifies the host:port of the server, not the client.] + found = False + for k, _v in headers: + if k.lower() == 'host': + found = True + break + if not found: + if port == 80: + headers.append(('Host', host)) + else: + headers.append(('Host', '%s:%s' % (host, port))) + + if method in methods_with_bodies: + # Stick in default type and length headers if not present + found = False + for k, v in headers: + if k.lower() == 'content-type': + found = True + break + if not found: + headers.append( + ('Content-Type', 'application/x-www-form-urlencoded'), + ) + headers.append(('Content-Length', str(len(body or '')))) + + return headers + + +def shb(response): + """Return status, headers, body the way we like from a response.""" + resp_status_line = '%s %s' % (response.status, response.reason) + + return resp_status_line, response.getheaders(), response.read() + + +def openURL(*args, raise_subcls=(), **kwargs): + """ + Open a URL, retrying when it fails. + + Specify ``raise_subcls`` (class or tuple of classes) to exclude + those socket.error subclasses from being suppressed and retried. + """ + opener = functools.partial(_open_url_once, *args, **kwargs) + + def on_exception(): + exc = sys.exc_info()[1] + if isinstance(exc, raise_subcls): + raise exc + time.sleep(0.5) + + # Try up to 10 times + return jaraco.functools.retry_call( + opener, + retries=9, + cleanup=on_exception, + trap=socket.error, + ) + + +def _open_url_once( + url, headers=None, method='GET', body=None, + host='127.0.0.1', port=8000, http_conn=http.client.HTTPConnection, + protocol='HTTP/1.1', ssl_context=None, +): + """Open the given HTTP resource and return status, headers, and body.""" + headers = cleanHeaders(headers, method, body, host, port) + + # Allow http_conn to be a class or an instance + if hasattr(http_conn, 'host'): + conn = http_conn + else: + kw = {} + if ssl_context: + kw['context'] = ssl_context + conn = http_conn(interface(host), port, **kw) + conn._http_vsn_str = protocol + conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()])) + if isinstance(url, bytes): + url = url.decode() + conn.putrequest( + method.upper(), url, skip_host=True, + skip_accept_encoding=True, + ) + for key, value in headers: + conn.putheader(key, value.encode('Latin-1')) + conn.endheaders() + if body is not None: + conn.send(body) + # Handle response + response = conn.getresponse() + s, h, b = shb(response) + if not hasattr(http_conn, 'host'): + # We made our own conn instance. Close it. + conn.close() + return s, h, b + + +def strip_netloc(url): + """Return absolute-URI path from URL. + + Strip the scheme and host from the URL, returning the + server-absolute portion. + + Useful for wrapping an absolute-URI for which only the + path is expected (such as in calls to :py:meth:`WebCase.getPage`). + + .. testsetup:: + + from cheroot.test.webtest import strip_netloc + + >>> strip_netloc('https://google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('//google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('/foo/bar?bing#baz') + '/foo/bar?bing' + """ + parsed = urllib.parse.urlparse(url) + _scheme, _netloc, path, params, query, _fragment = parsed + stripped = '', '', path, params, query, '' + return urllib.parse.urlunparse(stripped) + + +# Add any exceptions which your web framework handles +# normally (that you don't want server_error to trap). +ignored_exceptions = [] + +# You'll want set this to True when you can't guarantee +# that each response will immediately follow each request; +# for example, when handling requests via multiple threads. +ignore_all = False + + +class ServerError(Exception): + """Exception for signalling server error.""" + + on = False + + +def server_error(exc=None): + """Server debug hook. + + Return True if exception handled, False if ignored. + You probably want to wrap this, so you can still handle an error using + your framework when it's ignored. + """ + if exc is None: + exc = sys.exc_info() + + if ignore_all or exc[0] in ignored_exceptions: + return False + else: + ServerError.on = True + print('') + print(''.join(traceback.format_exception(*exc))) + return True diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/testing.py b/awesome_venv/lib/python3.10/site-packages/cheroot/testing.py new file mode 100644 index 000000000..3e404e59e --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/testing.py @@ -0,0 +1,149 @@ +"""Pytest fixtures and other helpers for doing testing by end-users.""" + +from contextlib import closing, contextmanager +import errno +import socket +import threading +import time +import http.client + +import pytest + +import cheroot.server +from cheroot.test import webtest +import cheroot.wsgi + +EPHEMERAL_PORT = 0 +NO_INTERFACE = None # Using this or '' will cause an exception +ANY_INTERFACE_IPV4 = '0.0.0.0' +ANY_INTERFACE_IPV6 = '::' + +config = { + cheroot.wsgi.Server: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'wsgi_app': None, + }, + cheroot.server.HTTPServer: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'gateway': cheroot.server.Gateway, + }, +} + + +@contextmanager +def cheroot_server(server_factory): + """Set up and tear down a Cheroot server instance.""" + conf = config[server_factory].copy() + bind_port = conf.pop('bind_addr')[-1] + + for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4: + try: + actual_bind_addr = (interface, bind_port) + httpserver = server_factory( # create it + bind_addr=actual_bind_addr, + **conf + ) + except OSError: + pass + else: + break + + httpserver.shutdown_timeout = 0 # Speed-up tests teardown + + threading.Thread(target=httpserver.safe_start).start() # spawn it + while not httpserver.ready: # wait until fully initialized and bound + time.sleep(0.1) + + yield httpserver + + httpserver.stop() # destroy it + + +@pytest.fixture +def wsgi_server(): + """Set up and tear down a Cheroot WSGI server instance.""" + with cheroot_server(cheroot.wsgi.Server) as srv: + yield srv + + +@pytest.fixture +def native_server(): + """Set up and tear down a Cheroot HTTP server instance.""" + with cheroot_server(cheroot.server.HTTPServer) as srv: + yield srv + + +class _TestClient: + def __init__(self, server): + self._interface, self._host, self._port = _get_conn_data( + server.bind_addr, + ) + self.server_instance = server + self._http_connection = self.get_connection() + + def get_connection(self): + name = '{interface}:{port}'.format( + interface=self._interface, + port=self._port, + ) + conn_cls = ( + http.client.HTTPConnection + if self.server_instance.ssl_adapter is None else + http.client.HTTPSConnection + ) + return conn_cls(name) + + def request( + self, uri, method='GET', headers=None, http_conn=None, + protocol='HTTP/1.1', + ): + return webtest.openURL( + uri, method=method, + headers=headers, + host=self._host, port=self._port, + http_conn=http_conn or self._http_connection, + protocol=protocol, + ) + + def __getattr__(self, attr_name): + def _wrapper(uri, **kwargs): + http_method = attr_name.upper() + return self.request(uri, method=http_method, **kwargs) + + return _wrapper + + +def _probe_ipv6_sock(interface): + # Alternate way is to check IPs on interfaces using glibc, like: + # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py + try: + with closing(socket.socket(family=socket.AF_INET6)) as sock: + sock.bind((interface, 0)) + except OSError as sock_err: + if sock_err.errno != errno.EADDRNOTAVAIL: + raise + else: + return True + + return False + + +def _get_conn_data(bind_addr): + if isinstance(bind_addr, tuple): + host, port = bind_addr + else: + host, port = bind_addr, 0 + + interface = webtest.interface(host) + + if ':' in interface and not _probe_ipv6_sock(interface): + interface = '127.0.0.1' + if ':' in host: + host = interface + + return interface, host, port + + +def get_server_client(server): + """Create and return a test client for the given server.""" + return _TestClient(server) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/testing.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/testing.pyi new file mode 100644 index 000000000..4c825f986 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/testing.pyi @@ -0,0 +1,17 @@ +from typing import Any, Iterator, Optional, TypeVar + +from .server import HTTPServer +from .wsgi import Server + +T = TypeVar('T', bound=HTTPServer) + +EPHEMERAL_PORT: int +NO_INTERFACE: Optional[str] +ANY_INTERFACE_IPV4: str +ANY_INTERFACE_IPV6: str +config: dict + +def cheroot_server(server_factory: T) -> Iterator[T]: ... +def wsgi_server() -> Iterator[Server]: ... +def native_server() -> Iterator[HTTPServer]: ... +def get_server_client(server) -> Any: ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__init__.py b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__init__.py new file mode 100644 index 000000000..098b8f25f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__init__.py @@ -0,0 +1 @@ +"""HTTP workers pool.""" diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__init__.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..ca160e5e8 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/threadpool.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/threadpool.cpython-310.pyc new file mode 100644 index 000000000..478ae0983 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/__pycache__/threadpool.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.py b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.py new file mode 100644 index 000000000..3437d9bda --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.py @@ -0,0 +1,337 @@ +"""A thread-based worker pool. + +.. spelling:: + + joinable +""" + +import collections +import threading +import time +import socket +import warnings +import queue + +from jaraco.functools import pass_none + + +__all__ = ('WorkerThread', 'ThreadPool') + + +class TrueyZero: + """Object which equals and does math like the integer 0 but evals True.""" + + def __add__(self, other): + return other + + def __radd__(self, other): + return other + + +trueyzero = TrueyZero() + +_SHUTDOWNREQUEST = None + + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + def __init__(self, server): + """Initialize WorkerThread instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + """ + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ( + self.start_time is None + and trueyzero + or self.conn.requests_seen + ), + 'Bytes Read': lambda s: self.bytes_read + ( + self.start_time is None + and trueyzero + or self.conn.rfile.bytes_read + ), + 'Bytes Written': lambda s: self.bytes_written + ( + self.start_time is None + and trueyzero + or self.conn.wfile.bytes_written + ), + 'Work Time': lambda s: self.work_time + ( + self.start_time is None + and trueyzero + or time.time() - self.start_time + ), + 'Read Throughput': lambda s: s['Bytes Read'](s) / ( + s['Work Time'](s) or 1e-6 + ), + 'Write Throughput': lambda s: s['Bytes Written'](s) / ( + s['Work Time'](s) or 1e-6 + ), + } + threading.Thread.__init__(self) + + def run(self): + """Process incoming HTTP connections. + + Retrieves incoming connections from thread pool. + """ + self.server.stats['Worker Threads'][self.name] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + is_stats_enabled = self.server.stats['Enabled'] + if is_stats_enabled: + self.start_time = time.time() + keep_conn_open = False + try: + keep_conn_open = conn.communicate() + finally: + if keep_conn_open: + self.server.put_conn(conn) + else: + conn.close() + if is_stats_enabled: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit) as ex: + self.server.interrupt = ex + + +class ThreadPool: + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__( + self, server, min=10, max=-1, accepted_queue_size=-1, + accepted_queue_timeout=10, + ): + """Initialize HTTP requests queue instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + min (int): minimum number of worker threads + max (int): maximum number of worker threads (-1/inf for no max) + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + + :raises ValueError: if the min/max values are invalid + :raises TypeError: if the max is not an integer or inf + """ + if min < 1: + raise ValueError(f'min={min!s} must be > 0') + + if max == float('inf'): + pass + elif not isinstance(max, int) or max == 0: + raise TypeError( + 'Expected an integer or the infinity value for the `max` ' + f'argument but got {max!r}.', + ) + elif max < 0: + max = float('inf') + + if max < min: + raise ValueError( + f'max={max!s} must be > min={min!s} (or infinity for no max)', + ) + + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue(maxsize=accepted_queue_size) + self._queue_put_timeout = accepted_queue_timeout + self.get = self._queue.get + self._pending_shutdowns = collections.deque() + + def start(self): + """Start the pool of threads. + + :raises RuntimeError: if the pool is already started + """ + if self._threads: + raise RuntimeError('Threadpools can only be started once.') + self.grow(self.min) + + @property + def idle(self): # noqa: D401; irrelevant for properties + """Number of worker threads which are idle. Read-only.""" # noqa: D401 + idles = len([t for t in self._threads if t.conn is None]) + return max(idles - len(self._pending_shutdowns), 0) + + def put(self, obj): + """Put request into queue. + + Args: + obj (:py:class:`~cheroot.server.HTTPConnection`): HTTP connection + waiting to be processed + """ + self._queue.put(obj, block=True, timeout=self._queue_put_timeout) + + def _clear_dead_threads(self): + # Remove any dead threads from our list + for t in [t for t in self._threads if not t.is_alive()]: + self._threads.remove(t) + try: + self._pending_shutdowns.popleft() + except IndexError: + pass + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + budget = max(self.max - len(self._threads), 0) + n_new = min(amount, budget) + + workers = [self._spawn_worker() for i in range(n_new)] + for worker in workers: + while not worker.ready: + time.sleep(.1) + self._threads.extend(workers) + + def _spawn_worker(self): + worker = WorkerThread(self.server) + worker.name = ( + 'CP Server {worker_name!s}'. + format(worker_name=worker.name) + ) + worker.start() + return worker + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + amount -= len(self._pending_shutdowns) + self._clear_dead_threads() + if amount <= 0: + return + + # calculate the number of threads above the minimum + n_extra = max(len(self._threads) - self.min, 0) + + # don't remove more than amount + n_to_remove = min(amount, n_extra) + + # put shutdown requests on the queue equal to the number of threads + # to remove. As each request is processed by a worker, that worker + # will terminate and be culled from the list. + for _ in range(n_to_remove): + self._pending_shutdowns.append(None) + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + """Terminate all worker threads. + + Args: + timeout (int): time to wait for threads to stop gracefully + """ + # for compatability, negative timeouts are treated like None + # TODO: treat negative timeouts like already expired timeouts + if timeout is not None and timeout < 0: + timeout = None + warnings.warning( + 'In the future, negative timeouts to Server.stop() ' + 'will be equivalent to a timeout of zero.', + stacklevel=2, + ) + + if timeout is not None: + endtime = time.time() + timeout + + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + ignored_errors = ( + # Raised when start_response called >1 time w/o exc_info or + # wsgi write is called before start_response. See cheroot#261 + RuntimeError, + # Ignore repeated Ctrl-C. See cherrypy#691. + KeyboardInterrupt, + ) + + for worker in self._clear_threads(): + remaining_time = timeout and endtime - time.time() + try: + worker.join(remaining_time) + if worker.is_alive(): + # Timeout exhausted; forcibly shut down the socket. + self._force_close(worker.conn) + worker.join() + except ignored_errors: + pass + + @staticmethod + @pass_none + def _force_close(conn): + if conn.rfile.closed: + return + try: + try: + conn.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + conn.socket.shutdown() + except OSError: + # shutdown sometimes fails (race with 'closed' check?) + # ref #238 + pass + + def _clear_threads(self): + """Clear self._threads and yield all joinable threads.""" + # threads = pop_all(self._threads) + threads, self._threads[:] = self._threads[:], [] + return ( + thread + for thread in threads + if thread is not threading.current_thread() + ) + + @property + def qsize(self): + """Return the queue size.""" + return self._queue.qsize() diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.pyi new file mode 100644 index 000000000..201d39140 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/workers/threadpool.pyi @@ -0,0 +1,37 @@ +import threading +from typing import Any + +class TrueyZero: + def __add__(self, other): ... + def __radd__(self, other): ... + +trueyzero: TrueyZero + +class WorkerThread(threading.Thread): + conn: Any + server: Any + ready: bool + requests_seen: int + bytes_read: int + bytes_written: int + start_time: Any + work_time: int + stats: Any + def __init__(self, server): ... + def run(self) -> None: ... + +class ThreadPool: + server: Any + min: Any + max: Any + get: Any + def __init__(self, server, min: int = ..., max: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ...) -> None: ... + def start(self) -> None: ... + @property + def idle(self): ... + def put(self, obj) -> None: ... + def grow(self, amount) -> None: ... + def shrink(self, amount) -> None: ... + def stop(self, timeout: int = ...) -> None: ... + @property + def qsize(self) -> int: ... diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.py b/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.py new file mode 100644 index 000000000..1dbe10ee2 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.py @@ -0,0 +1,412 @@ +"""This class holds Cheroot WSGI server implementation. + +Simplest example on how to use this server:: + + from cheroot import wsgi + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return [b'Hello world!'] + + addr = '0.0.0.0', 8070 + server = wsgi.Server(addr, my_crazy_app) + server.start() + +The Cheroot WSGI server can serve as many WSGI applications +as you want in one instance by using a PathInfoDispatcher:: + + path_map = { + '/': my_crazy_app, + '/blog': my_blog_app, + } + d = wsgi.PathInfoDispatcher(path_map) + server = wsgi.Server(addr, d) +""" + +import sys + +from . import server +from .workers import threadpool +from ._compat import ntob, bton + + +class Server(server.HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__( + self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5, + accepted_queue_size=-1, accepted_queue_timeout=10, + peercreds_enabled=False, peercreds_resolve_enabled=False, + reuse_port=False, + ): + """Initialize WSGI Server instance. + + Args: + bind_addr (tuple): network interface to listen to + wsgi_app (callable): WSGI application callable + numthreads (int): number of threads for WSGI thread pool + server_name (str): web server name to be advertised via + Server HTTP header + max (int): maximum number of worker threads + request_queue_size (int): the 'backlog' arg to + socket.listen(); max queued connections + timeout (int): the timeout in seconds for accepted connections + shutdown_timeout (int): the total time, in seconds, to + wait for worker threads to cleanly exit + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + super(Server, self).__init__( + bind_addr, + gateway=wsgi_gateways[self.wsgi_version], + server_name=server_name, + peercreds_enabled=peercreds_enabled, + peercreds_resolve_enabled=peercreds_resolve_enabled, + reuse_port=reuse_port, + ) + self.wsgi_app = wsgi_app + self.request_queue_size = request_queue_size + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.requests = threadpool.ThreadPool( + self, min=numthreads or 1, max=max, + accepted_queue_size=accepted_queue_size, + accepted_queue_timeout=accepted_queue_timeout, + ) + + @property + def numthreads(self): + """Set minimum number of threads.""" + return self.requests.min + + @numthreads.setter + def numthreads(self, value): + self.requests.min = value + + +class Gateway(server.Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + """Initialize WSGI Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + super(Gateway, self).__init__(req) + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + @classmethod + def gateway_map(cls): + """Create a mapping of gateways and their versions. + + Returns: + dict[tuple[int,int],class]: map of gateway version and + corresponding class + + """ + return {gw.version: gw for gw in cls.__subclasses__()} + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + raise NotImplementedError # pragma: no cover + + def respond(self): + """Process the current request. + + From :pep:`333`: + + The start_response callable must not actually transmit + the response headers. Instead, it must store them for the + server or gateway to transmit only after the first + iteration of the application return value that yields + a NON-EMPTY string, or upon the application's first + invocation of the write() callable. + """ + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in filter(None, response): + if not isinstance(chunk, bytes): + raise ValueError('WSGI Applications must yield bytes') + self.write(chunk) + finally: + # Send headers if not already sent + self.req.ensure_headers_sent() + if hasattr(response, 'close'): + response.close() + + def start_response(self, status, headers, exc_info=None): # noqa: WPS238 + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise RuntimeError( + 'WSGI start_response called a second ' + 'time with no exc_info.', + ) + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + value = exc_info[1] + raise value + + self.req.status = self._encode_status(status) + + for k, v in headers: + if not isinstance(k, str): + raise TypeError( + 'WSGI response header key %r is not of type str.' % k, + ) + if not isinstance(v, str): + raise TypeError( + 'WSGI response header value %r is not of type str.' % v, + ) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + out_header = ntob(k), ntob(v) + self.req.outheaders.append(out_header) + + return self.write + + @staticmethod + def _encode_status(status): + """Cast status to bytes representation of current Python version. + + According to :pep:`3333`, when using Python 3, the response status + and headers must be bytes masquerading as Unicode; that is, they + must be of type "str" but are restricted to code points in the + "Latin-1" set. + """ + if not isinstance(status, str): + raise TypeError('WSGI response status is not of type str.') + return status.encode('ISO-8859-1') + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise RuntimeError('WSGI write called before start_response.') + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response( + '500 Internal Server Error', + 'The requested resource returned more bytes than the ' + 'declared Content-Length.', + ) + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + self.req.ensure_headers_sent() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + 'Response body exceeds the declared Content-Length.', + ) + + +class Gateway_10(Gateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + version = 1, 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + req_conn = req.conn + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': bton(req.path), + 'QUERY_STRING': bton(req.qs), + 'REMOTE_ADDR': req_conn.remote_addr or '', + 'REMOTE_PORT': str(req_conn.remote_port or ''), + 'REQUEST_METHOD': bton(req.method), + 'REQUEST_URI': bton(req.uri), + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': bton(req.request_protocol), + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.input_terminated': bool(req.chunked_read), + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': bton(req.scheme), + 'wsgi.version': self.version, + } + + if isinstance(req.server.bind_addr, str): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env['SERVER_PORT'] = '' + try: + env['X_REMOTE_PID'] = str(req_conn.peer_pid) + env['X_REMOTE_UID'] = str(req_conn.peer_uid) + env['X_REMOTE_GID'] = str(req_conn.peer_gid) + + env['X_REMOTE_USER'] = str(req_conn.peer_user) + env['X_REMOTE_GROUP'] = str(req_conn.peer_group) + + env['REMOTE_USER'] = env['X_REMOTE_USER'] + except RuntimeError: + """Unable to retrieve peer creds data. + + Unsupported by current kernel or socket error happened, or + unsupported socket type, or disabled. + """ + else: + env['SERVER_PORT'] = str(req.server.bind_addr[1]) + + # Request headers + env.update( + ( + 'HTTP_{header_name!s}'. + format(header_name=bton(k).upper().replace('-', '_')), + bton(v), + ) + for k, v in req.inheaders.items() + ) + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop('HTTP_CONTENT_TYPE', None) + if ct is not None: + env['CONTENT_TYPE'] = ct + cl = env.pop('HTTP_CONTENT_LENGTH', None) + if cl is not None: + env['CONTENT_LENGTH'] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class Gateway_u0(Gateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses Unicode for keys + and values in both Python 2 and Python 3. + """ + + version = 'u', 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + env_10 = super(Gateway_u0, self).get_environ() + env = dict(env_10.items()) + + # Request-URI + enc = env.setdefault('wsgi.url_encoding', 'utf-8') + try: + env['PATH_INFO'] = req.path.decode(enc) + env['QUERY_STRING'] = req.qs.decode(enc) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env['wsgi.url_encoding'] = 'ISO-8859-1' + env['PATH_INFO'] = env_10['PATH_INFO'] + env['QUERY_STRING'] = env_10['QUERY_STRING'] + + env.update(env.items()) + + return env + + +wsgi_gateways = Gateway.gateway_map() + + +class PathInfoDispatcher: + """A WSGI dispatcher for dispatch based on the PATH_INFO.""" + + def __init__(self, apps): + """Initialize path info WSGI app dispatcher. + + Args: + apps (dict[str,object]|list[tuple[str,object]]): URI prefix + and WSGI app pairs + """ + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + def by_path_len(app): + return len(app[0]) + apps.sort(key=by_path_len, reverse=True) + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip('/'), a) for p, a in apps] + + def __call__(self, environ, start_response): + """Process incoming WSGI request. + + Ref: :pep:`3333` + + Args: + environ (Mapping): a dict containing WSGI environment variables + start_response (callable): function, which sets response + status and headers + + Returns: + list[bytes]: iterable containing bytes to be returned in + HTTP response body + + """ + path = environ['PATH_INFO'] or '/' + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith('{path!s}/'.format(path=p)) or path == p: + environ = environ.copy() + environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + p + environ['PATH_INFO'] = path[len(p):] + return app(environ, start_response) + + start_response( + '404 Not Found', [ + ('Content-Type', 'text/plain'), + ('Content-Length', '0'), + ], + ) + return [''] + + +# compatibility aliases +globals().update( + WSGIServer=Server, + WSGIGateway=Gateway, + WSGIGateway_u0=Gateway_u0, + WSGIGateway_10=Gateway_10, + WSGIPathInfoDispatcher=PathInfoDispatcher, +) diff --git a/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.pyi b/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.pyi new file mode 100644 index 000000000..f96a18f92 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cheroot/wsgi.pyi @@ -0,0 +1,49 @@ +from . import server +from typing import Any + +class Server(server.HTTPServer): + wsgi_version: Any + wsgi_app: Any + request_queue_size: Any + timeout: Any + shutdown_timeout: Any + requests: Any + def __init__(self, bind_addr, wsgi_app, numthreads: int = ..., server_name: Any | None = ..., max: int = ..., request_queue_size: int = ..., timeout: int = ..., shutdown_timeout: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ..., reuse_port: bool = ...) -> None: ... + @property + def numthreads(self): ... + @numthreads.setter + def numthreads(self, value) -> None: ... + +class Gateway(server.Gateway): + started_response: bool + env: Any + remaining_bytes_out: Any + def __init__(self, req) -> None: ... + @classmethod + def gateway_map(cls): ... + def get_environ(self) -> None: ... + def respond(self) -> None: ... + def start_response(self, status, headers, exc_info: Any | None = ...): ... + def write(self, chunk) -> None: ... + +class Gateway_10(Gateway): + version: Any + def get_environ(self): ... + +class Gateway_u0(Gateway_10): + version: Any + def get_environ(self): ... + +wsgi_gateways: Any + +class PathInfoDispatcher: + apps: Any + def __init__(self, apps): ... + def __call__(self, environ, start_response): ... + + +WSGIServer = Server +WSGIGateway = Gateway +WSGIGateway_u0 = Gateway_u0 +WSGIGateway_10 = Gateway_10 +WSGIPathInfoDispatcher = PathInfoDispatcher diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__init__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/__init__.py new file mode 100644 index 000000000..8e27c8121 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/__init__.py @@ -0,0 +1,370 @@ +"""CherryPy is a pythonic, object-oriented HTTP framework. + +CherryPy consists of not one, but four separate API layers. + +The APPLICATION LAYER is the simplest. CherryPy applications are written as +a tree of classes and methods, where each branch in the tree corresponds to +a branch in the URL path. Each method is a 'page handler', which receives +GET and POST params as keyword arguments, and returns or yields the (HTML) +body of the response. The special method name 'index' is used for paths +that end in a slash, and the special method name 'default' is used to +handle multiple paths via a single handler. This layer also includes: + + * the 'exposed' attribute (and cherrypy.expose) + * cherrypy.quickstart() + * _cp_config attributes + * cherrypy.tools (including cherrypy.session) + * cherrypy.url() + +The ENVIRONMENT LAYER is used by developers at all levels. It provides +information about the current request and response, plus the application +and server environment, via a (default) set of top-level objects: + + * cherrypy.request + * cherrypy.response + * cherrypy.engine + * cherrypy.server + * cherrypy.tree + * cherrypy.config + * cherrypy.thread_data + * cherrypy.log + * cherrypy.HTTPError, NotFound, and HTTPRedirect + * cherrypy.lib + +The EXTENSION LAYER allows advanced users to construct and share their own +plugins. It consists of: + + * Hook API + * Tool API + * Toolbox API + * Dispatch API + * Config Namespace API + +Finally, there is the CORE LAYER, which uses the core API's to construct +the default components which are available at higher layers. You can think +of the default components as the 'reference implementation' for CherryPy. +Megaframeworks (and advanced users) may replace the default components +with customized or extended components. The core API's are: + + * Application API + * Engine API + * Request API + * Server API + * WSGI API + +These API's are described in the `CherryPy specification +`_. +""" + +try: + import pkg_resources +except ImportError: + pass + +from threading import local as _local + +from ._cperror import ( + HTTPError, HTTPRedirect, InternalRedirect, + NotFound, CherryPyException, +) + +from . import _cpdispatch as dispatch + +from ._cptools import default_toolbox as tools, Tool +from ._helper import expose, popargs, url + +from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig + +import cherrypy.lib.httputil as _httputil + +from ._cptree import Application +from . import _cpwsgi as wsgi + +from . import process +try: + from .process import win32 + engine = win32.Win32Bus() + engine.console_control_handler = win32.ConsoleCtrlHandler(engine) + del win32 +except ImportError: + engine = process.bus + +from . import _cpchecker + +__all__ = ( + 'HTTPError', 'HTTPRedirect', 'InternalRedirect', + 'NotFound', 'CherryPyException', + 'dispatch', 'tools', 'Tool', 'Application', + 'wsgi', 'process', 'tree', 'engine', + 'quickstart', 'serving', 'request', 'response', 'thread_data', + 'log', 'expose', 'popargs', 'url', 'config', +) + + +__import__('cherrypy._cptools') +__import__('cherrypy._cprequest') + + +tree = _cptree.Tree() + + +try: + __version__ = pkg_resources.require('cherrypy')[0].version +except Exception: + __version__ = 'unknown' + + +engine.listeners['before_request'] = set() +engine.listeners['after_request'] = set() + + +engine.autoreload = process.plugins.Autoreloader(engine) +engine.autoreload.subscribe() + +engine.thread_manager = process.plugins.ThreadManager(engine) +engine.thread_manager.subscribe() + +engine.signal_handler = process.plugins.SignalHandler(engine) + + +class _HandleSignalsPlugin(object): + """Handle signals from other processes. + + Based on the configured platform handlers above. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Add the handlers based on the platform.""" + if hasattr(self.bus, 'signal_handler'): + self.bus.signal_handler.subscribe() + if hasattr(self.bus, 'console_control_handler'): + self.bus.console_control_handler.subscribe() + + +engine.signals = _HandleSignalsPlugin(engine) + + +server = _cpserver.Server() +server.subscribe() + + +def quickstart(root=None, script_name='', config=None): + """Mount the given root, start the builtin server (and engine), then block. + + root: an instance of a "controller class" (a collection of page handler + methods) which represents the root of the application. + script_name: a string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the URL + at which to mount the given root. For example, if root.index() will + handle requests to "http://www.example.com:8080/dept/app1/", then + the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the root + of the URI, it MUST be an empty string (not "/"). + config: a file or dict containing application config. If this contains + a [global] section, those entries will be used in the global + (site-wide) config. + """ + if config: + _global_conf_alias.update(config) + + tree.mount(root, script_name, config) + + engine.signals.subscribe() + engine.start() + engine.block() + + +class _Serving(_local): + """An interface for registering request and response objects. + + Rather than have a separate "thread local" object for the request and + the response, this class works as a single threadlocal container for + both objects (and any others which developers wish to define). In this + way, we can easily dump those objects when we stop/start a new HTTP + conversation, yet still refer to them as module-level globals in a + thread-safe way. + """ + + request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), + _httputil.Host('127.0.0.1', 1111)) + """ + The request object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + response = _cprequest.Response() + """ + The response object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + def load(self, request, response): + self.request = request + self.response = response + + def clear(self): + """Remove all attributes of self.""" + self.__dict__.clear() + + +serving = _Serving() + + +class _ThreadLocalProxy(object): + + __slots__ = ['__attrname__', '__dict__'] + + def __init__(self, attrname): + self.__attrname__ = attrname + + def __getattr__(self, name): + child = getattr(serving, self.__attrname__) + return getattr(child, name) + + def __setattr__(self, name, value): + if name in ('__attrname__', ): + object.__setattr__(self, name, value) + else: + child = getattr(serving, self.__attrname__) + setattr(child, name, value) + + def __delattr__(self, name): + child = getattr(serving, self.__attrname__) + delattr(child, name) + + @property + def __dict__(self): + child = getattr(serving, self.__attrname__) + d = child.__class__.__dict__.copy() + d.update(child.__dict__) + return d + + def __getitem__(self, key): + child = getattr(serving, self.__attrname__) + return child[key] + + def __setitem__(self, key, value): + child = getattr(serving, self.__attrname__) + child[key] = value + + def __delitem__(self, key): + child = getattr(serving, self.__attrname__) + del child[key] + + def __contains__(self, key): + child = getattr(serving, self.__attrname__) + return key in child + + def __len__(self): + child = getattr(serving, self.__attrname__) + return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) + # Python 3 + __bool__ = __nonzero__ + + +# Create request and response object (the same objects will be used +# throughout the entire life of the webserver, but will redirect +# to the "serving" object) +request = _ThreadLocalProxy('request') +response = _ThreadLocalProxy('response') + +# Create thread_data object as a thread-specific all-purpose storage + + +class _ThreadData(_local): + """A container for thread-specific data.""" + + +thread_data = _ThreadData() + + +# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. +# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. +# The only other way would be to change what is returned from type(request) +# and that's not possible in pure Python (you'd have to fake ob_type). +def _cherrypy_pydoc_resolve(thing, forceload=0): + """Given an object or a path to an object, get the object and its name.""" + if isinstance(thing, _ThreadLocalProxy): + thing = getattr(serving, thing.__attrname__) + return _pydoc._builtin_resolve(thing, forceload) + + +try: + import pydoc as _pydoc + _pydoc._builtin_resolve = _pydoc.resolve + _pydoc.resolve = _cherrypy_pydoc_resolve +except ImportError: + pass + + +class _GlobalLogManager(_cplogging.LogManager): + """A site-wide LogManager; routes to app.log or global log as appropriate. + + This :class:`LogManager` implements + cherrypy.log() and cherrypy.log.access(). If either + function is called during a request, the message will be sent to the + logger for the current Application. If they are called outside of a + request, the message will be sent to the site-wide logger. + """ + + def __call__(self, *args, **kwargs): + """Log the given message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + # Do NOT use try/except here. See + # https://github.com/cherrypy/cherrypy/issues/945 + if hasattr(request, 'app') and hasattr(request.app, 'log'): + log = request.app.log + else: + log = self + return log.error(*args, **kwargs) + + def access(self): + """Log an access message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + try: + return request.app.log.access() + except AttributeError: + return _cplogging.LogManager.access(self) + + +log = _GlobalLogManager() +# Set a default screen handler on the global log. +log.screen = True +log.error_file = '' +# Using an access file makes CP about 10% slower. Leave off by default. +log.access_file = '' + + +@engine.subscribe('log') +def _buslog(msg, level): + log.error(msg, 'ENGINE', severity=level) + + +# Use _global_conf_alias so quickstart can use 'config' as an arg +# without shadowing cherrypy.config. +config = _global_conf_alias = _cpconfig.Config() +config.defaults = { + 'tools.log_tracebacks.on': True, + 'tools.log_headers.on': True, + 'tools.trailing_slash.on': True, + 'tools.encode.on': True +} +config.namespaces['log'] = lambda k, v: setattr(log, k, v) +config.namespaces['checker'] = lambda k, v: setattr(checker, k, v) +# Must reset to get our defaults applied. +config.reset() + +checker = _cpchecker.Checker() +engine.subscribe('start', checker) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__main__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/__main__.py new file mode 100644 index 000000000..6674f7cb4 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/__main__.py @@ -0,0 +1,5 @@ +"""CherryPy'd cherryd daemon runner.""" +from cherrypy.daemon import run + + +__name__ == '__main__' and run() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..f8f4f40ce Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__main__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 000000000..0fc9a060e Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/__main__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpchecker.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpchecker.cpython-310.pyc new file mode 100644 index 000000000..c74438695 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpchecker.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpcompat.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpcompat.cpython-310.pyc new file mode 100644 index 000000000..a1c461b9d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpcompat.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpconfig.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpconfig.cpython-310.pyc new file mode 100644 index 000000000..12e15691d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpconfig.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpdispatch.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpdispatch.cpython-310.pyc new file mode 100644 index 000000000..7d6e59837 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpdispatch.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cperror.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cperror.cpython-310.pyc new file mode 100644 index 000000000..9e9f75427 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cperror.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cplogging.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cplogging.cpython-310.pyc new file mode 100644 index 000000000..49bc95924 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cplogging.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpmodpy.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpmodpy.cpython-310.pyc new file mode 100644 index 000000000..9d0eda811 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpmodpy.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpnative_server.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpnative_server.cpython-310.pyc new file mode 100644 index 000000000..bd3b442b0 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpnative_server.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpreqbody.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpreqbody.cpython-310.pyc new file mode 100644 index 000000000..367eca11c Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpreqbody.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cprequest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cprequest.cpython-310.pyc new file mode 100644 index 000000000..2abdedf51 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cprequest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpserver.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpserver.cpython-310.pyc new file mode 100644 index 000000000..1fa0ae18b Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpserver.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptools.cpython-310.pyc new file mode 100644 index 000000000..144e8e3bb Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptree.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptree.cpython-310.pyc new file mode 100644 index 000000000..ca5af23d2 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cptree.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi.cpython-310.pyc new file mode 100644 index 000000000..2e91190d5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi_server.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi_server.cpython-310.pyc new file mode 100644 index 000000000..246cc245f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_cpwsgi_server.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_helper.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_helper.cpython-310.pyc new file mode 100644 index 000000000..41533b177 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_helper.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_json.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_json.cpython-310.pyc new file mode 100644 index 000000000..e161c43f4 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/_json.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/daemon.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/daemon.cpython-310.pyc new file mode 100644 index 000000000..f9f2fc564 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/__pycache__/daemon.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpchecker.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpchecker.py new file mode 100644 index 000000000..f26f319cf --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpchecker.py @@ -0,0 +1,323 @@ +"""Checker for CherryPy sites and mounted apps.""" +import os +import warnings +import builtins + +import cherrypy + + +class Checker(object): + """A checker for CherryPy sites and their mounted applications. + + When this object is called at engine startup, it executes each + of its own methods whose names start with ``check_``. If you wish + to disable selected checks, simply add a line in your global + config which sets the appropriate method to False:: + + [global] + checker.check_skipped_app_config = False + + You may also dynamically add or replace ``check_*`` methods in this way. + """ + + on = True + """If True (the default), run all checks; if False, turn off all checks.""" + + def __init__(self): + """Initialize Checker instance.""" + self._populate_known_types() + + def __call__(self): + """Run all check_* methods.""" + if self.on: + oldformatwarning = warnings.formatwarning + warnings.formatwarning = self.formatwarning + try: + for name in dir(self): + if name.startswith('check_'): + method = getattr(self, name) + if method and hasattr(method, '__call__'): + method() + finally: + warnings.formatwarning = oldformatwarning + + def formatwarning(self, message, category, filename, lineno, line=None): + """Format a warning.""" + return 'CherryPy Checker:\n%s\n\n' % message + + # This value should be set inside _cpconfig. + global_config_contained_paths = False + + def check_app_config_entries_dont_start_with_script_name(self): + """Check for App config with sections that repeat script_name.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + if sn == '': + continue + sn_atoms = sn.strip('/').split('/') + for key in app.config.keys(): + key_atoms = key.strip('/').split('/') + if key_atoms[:len(sn_atoms)] == sn_atoms: + warnings.warn( + 'The application mounted at %r has config ' + 'entries that start with its script name: %r' % (sn, + key)) + + def check_site_config_entries_in_app_config(self): + """Check for mounted Applications that have site-scoped config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + + msg = [] + for section, entries in app.config.items(): + if section.startswith('/'): + for key, value in entries.items(): + for n in ('engine.', 'server.', 'tree.', 'checker.'): + if key.startswith(n): + msg.append('[%s] %s = %s' % + (section, key, value)) + if msg: + msg.insert(0, + 'The application mounted at %r contains the ' + 'following config entries, which are only allowed ' + 'in site-wide config. Move them to a [global] ' + 'section and pass them to cherrypy.config.update() ' + 'instead of tree.mount().' % sn) + warnings.warn(os.linesep.join(msg)) + + def check_skipped_app_config(self): + """Check for mounted Applications that have no config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + msg = 'The Application mounted at %r has an empty config.' % sn + if self.global_config_contained_paths: + msg += (' It looks like the config you passed to ' + 'cherrypy.config.update() contains application-' + 'specific sections. You must explicitly pass ' + 'application config via ' + 'cherrypy.tree.mount(..., config=app_config)') + warnings.warn(msg) + return + + def check_app_config_brackets(self): + """Check for App config with extraneous brackets in section names.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + for key in app.config.keys(): + if key.startswith('[') or key.endswith(']'): + warnings.warn( + 'The application mounted at %r has config ' + 'section names with extraneous brackets: %r. ' + 'Config *files* need brackets; config *dicts* ' + '(e.g. passed to tree.mount) do not.' % (sn, key)) + + def check_static_paths(self): + """Check Application config for incorrect static paths.""" + # Use the dummy Request object in the main thread. + request = cherrypy.request + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + request.app = app + for section in app.config: + # get_resource will populate request.config + request.get_resource(section + '/dummy.html') + conf = request.config.get + + if conf('tools.staticdir.on', False): + msg = '' + root = conf('tools.staticdir.root') + dir = conf('tools.staticdir.dir') + if dir is None: + msg = 'tools.staticdir.dir is not set.' + else: + fulldir = '' + if os.path.isabs(dir): + fulldir = dir + if root: + msg = ('dir is an absolute path, even ' + 'though a root is provided.') + testdir = os.path.join(root, dir[1:]) + if os.path.exists(testdir): + msg += ( + '\nIf you meant to serve the ' + 'filesystem folder at %r, remove the ' + 'leading slash from dir.' % (testdir,)) + else: + if not root: + msg = ( + 'dir is a relative path and ' + 'no root provided.') + else: + fulldir = os.path.join(root, dir) + if not os.path.isabs(fulldir): + msg = ('%r is not an absolute path.' % ( + fulldir,)) + + if fulldir and not os.path.exists(fulldir): + if msg: + msg += '\n' + msg += ('%r (root + dir) is not an existing ' + 'filesystem path.' % fulldir) + + if msg: + warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r' + % (msg, section, root, dir)) + + # -------------------------- Compatibility -------------------------- # + obsolete = { + 'server.default_content_type': 'tools.response_headers.headers', + 'log_access_file': 'log.access_file', + 'log_config_options': None, + 'log_file': 'log.error_file', + 'log_file_not_found': None, + 'log_request_headers': 'tools.log_headers.on', + 'log_to_screen': 'log.screen', + 'show_tracebacks': 'request.show_tracebacks', + 'throw_errors': 'request.throw_errors', + 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' + 'cherrypy.Application(Root())))'), + } + + deprecated = {} + + def _compat(self, config): + """Process config and warn on each obsolete or deprecated entry.""" + for section, conf in config.items(): + if isinstance(conf, dict): + for k in conf: + if k in self.obsolete: + warnings.warn('%r is obsolete. Use %r instead.\n' + 'section: [%s]' % + (k, self.obsolete[k], section)) + elif k in self.deprecated: + warnings.warn('%r is deprecated. Use %r instead.\n' + 'section: [%s]' % + (k, self.deprecated[k], section)) + else: + if section in self.obsolete: + warnings.warn('%r is obsolete. Use %r instead.' + % (section, self.obsolete[section])) + elif section in self.deprecated: + warnings.warn('%r is deprecated. Use %r instead.' + % (section, self.deprecated[section])) + + def check_compatibility(self): + """Process config and warn on each obsolete or deprecated entry.""" + self._compat(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._compat(app.config) + + # ------------------------ Known Namespaces ------------------------ # + extra_config_namespaces = [] + + def _known_ns(self, app): + ns = ['wsgi'] + ns.extend(app.toolboxes) + ns.extend(app.namespaces) + ns.extend(app.request_class.namespaces) + ns.extend(cherrypy.config.namespaces) + ns += self.extra_config_namespaces + + for section, conf in app.config.items(): + is_path_section = section.startswith('/') + if is_path_section and isinstance(conf, dict): + for k in conf: + atoms = k.split('.') + if len(atoms) > 1: + if atoms[0] not in ns: + # Spit out a special warning if a known + # namespace is preceded by "cherrypy." + if atoms[0] == 'cherrypy' and atoms[1] in ns: + msg = ( + 'The config entry %r is invalid; ' + 'try %r instead.\nsection: [%s]' + % (k, '.'.join(atoms[1:]), section)) + else: + msg = ( + 'The config entry %r is invalid, ' + 'because the %r config namespace ' + 'is unknown.\n' + 'section: [%s]' % (k, atoms[0], section)) + warnings.warn(msg) + elif atoms[0] == 'tools': + if atoms[1] not in dir(cherrypy.tools): + msg = ( + 'The config entry %r may be invalid, ' + 'because the %r tool was not found.\n' + 'section: [%s]' % (k, atoms[1], section)) + warnings.warn(msg) + + def check_config_namespaces(self): + """Process config and warn on each unknown config namespace.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_ns(app) + + # -------------------------- Config Types -------------------------- # + known_config_types = {} + + def _populate_known_types(self): + b = [x for x in vars(builtins).values() + if type(x) is type(str)] + + def traverse(obj, namespace): + for name in dir(obj): + # Hack for 3.2's warning about body_params + if name == 'body_params': + continue + vtype = type(getattr(obj, name, None)) + if vtype in b: + self.known_config_types[namespace + '.' + name] = vtype + + traverse(cherrypy.request, 'request') + traverse(cherrypy.response, 'response') + traverse(cherrypy.server, 'server') + traverse(cherrypy.engine, 'engine') + traverse(cherrypy.log, 'log') + + def _known_types(self, config): + msg = ('The config entry %r in section %r is of type %r, ' + 'which does not match the expected type %r.') + + for section, conf in config.items(): + if not isinstance(conf, dict): + conf = {section: conf} + for k, v in conf.items(): + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + + def check_config_types(self): + """Assert that config values are of the same type as default values.""" + self._known_types(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_types(app.config) + + # -------------------- Specific config warnings -------------------- # + def check_localhost(self): + """Warn if any socket_host is 'localhost'. See #711.""" + for k, v in cherrypy.config.items(): + if k == 'server.socket_host' and v == 'localhost': + warnings.warn("The use of 'localhost' as a socket host can " + 'cause problems on newer systems, since ' + "'localhost' can map to either an IPv4 or an " + "IPv6 address. You should use '127.0.0.1' " + "or '[::1]' instead.") diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpcompat.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpcompat.py new file mode 100644 index 000000000..a43f6d369 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpcompat.py @@ -0,0 +1,59 @@ +"""Compatibility code for using CherryPy with various versions of Python. + +To retain compatibility with older Python versions, this module provides a +useful abstraction over the differences between Python versions, sometimes by +preferring a newer idiom, sometimes an older one, and sometimes a custom one. + +In particular, Python 2 uses str and '' for byte strings, while Python 3 +uses str and '' for unicode strings. We will call each of these the 'native +string' type for each version. Because of this major difference, this module +provides +two functions: 'ntob', which translates native strings (of type 'str') into +byte strings regardless of Python version, and 'ntou', which translates native +strings to unicode strings. + +Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'. +They were created with Python 2.3-2.5 compatibility in mind. +Instead, use unicode literals (from __future__) and bytes literals +and their .encode/.decode methods as needed. +""" + +import http.client + + +def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string 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 given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n + + +def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n + + +def assert_native(n): + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) + + +# Some platforms don't expose HTTPSConnection, so handle it separately +HTTPSConnection = getattr(http.client, 'HTTPSConnection', None) + + +text_or_bytes = str, bytes diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpconfig.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpconfig.py new file mode 100644 index 000000000..8e3fd6120 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpconfig.py @@ -0,0 +1,296 @@ +""" +Configuration system for CherryPy. + +Configuration in CherryPy is implemented via dictionaries. Keys are strings +which name the mapped value, which may be of any type. + + +Architecture +------------ + +CherryPy Requests are part of an Application, which runs in a global context, +and configuration data may apply to any of those three scopes: + +Global + Configuration entries which apply everywhere are stored in + cherrypy.config. + +Application + Entries which apply to each mounted application are stored + on the Application object itself, as 'app.config'. This is a two-level + dict where each key is a path, or "relative URL" (for example, "/" or + "/path/to/my/page"), and each value is a config dict. Usually, this + data is provided in the call to tree.mount(root(), config=conf), + although you may also use app.merge(conf). + +Request + Each Request object possesses a single 'Request.config' dict. + Early in the request process, this dict is populated by merging global + config entries, Application entries (whose path equals or is a parent + of Request.path_info), and any config acquired while looking up the + page handler (see next). + + +Declaration +----------- + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, CherryPy +uses Python's builtin ConfigParser; you declare Application config by +writing each path as a section header:: + + [/path/to/my/page] + request.stream = True + +To declare global configuration entries, place them in a [global] section. + +You may also declare config entries directly on the classes and methods +(page handlers) that make up your CherryPy application via the ``_cp_config`` +attribute, set with the ``cherrypy.config`` decorator. For example:: + + @cherrypy.config(**{'tools.gzip.on': True}) + class Demo: + + @cherrypy.expose + @cherrypy.config(**{'request.show_tracebacks': False}) + def index(self): + return "Hello world" + +.. note:: + + This behavior is only guaranteed for the default dispatcher. + Other dispatchers may have different restrictions on where + you can attach config attributes. + + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. +Current namespaces: + +engine + Controls the 'application engine', including autoreload. + These can only be declared in the global config. + +tree + Grafts cherrypy.Application objects onto cherrypy.tree. + These can only be declared in the global config. + +hooks + Declares additional request-processing functions. + +log + Configures the logging for each application. + These can only be declared in the global or / config. + +request + Adds attributes to each Request. + +response + Adds attributes to each Response. + +server + Controls the default HTTP server via cherrypy.server. + These can only be declared in the global config. + +tools + Runs and configures additional request-processing packages. + +wsgi + Adds WSGI middleware to an Application's "pipeline". + These can only be declared in the app's root config ("/"). + +checker + Controls the 'checker', which looks for common errors in + app state (including config) when the engine starts. + Global config only. + +The only key that does not exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +cherrypy._cpconfig.environments[environment]. It only applies to the global +config, and only when you use cherrypy.config.update. + +You can define your own namespaces to be called at the Global, Application, +or Request level, by adding a named handler to cherrypy.config.namespaces, +app.namespaces, or app.request_class.namespaces. The name can +be any string, and the handler must be either a callable or a (Python 2.5 +style) context manager. +""" + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import reprconf + + +def _if_filename_register_autoreload(ob): + """Register for autoreload if ob is a string (presumed filename).""" + is_filename = isinstance(ob, text_or_bytes) + is_filename and cherrypy.engine.autoreload.files.add(ob) + + +def merge(base, other): + """Merge one app config (from a dict, file, or filename) into another. + + If the given config is a filename, it will be appended to + the list of files to monitor for "autoreload" changes. + """ + _if_filename_register_autoreload(other) + + # Load other into base + for section, value_map in reprconf.Parser.load(other).items(): + if not isinstance(value_map, dict): + raise ValueError( + 'Application config must include section headers, but the ' + "config you tried to merge doesn't have any sections. " + 'Wrap your config in another dict with paths as section ' + "headers, for example: {'/': config}.") + base.setdefault(section, {}).update(value_map) + + +class Config(reprconf.Config): + """The 'global' configuration data for the entire CherryPy process.""" + + def update(self, config): + """Update self from a dict, file or filename.""" + _if_filename_register_autoreload(config) + super(Config, self).update(config) + + def _apply(self, config): + """Update self from a dict.""" + if isinstance(config.get('global'), dict): + if len(config) > 1: + cherrypy.checker.global_config_contained_paths = True + config = config['global'] + if 'tools.staticdir.dir' in config: + config['tools.staticdir.section'] = 'global' + super(Config, self)._apply(config) + + @staticmethod + def __call__(**kwargs): + """Decorate for page handlers to set _cp_config.""" + def tool_decorator(f): + _Vars(f).setdefault('_cp_config', {}).update(kwargs) + return f + return tool_decorator + + +class _Vars(object): + """Adapter allowing setting a default attribute on a function or class.""" + + def __init__(self, target): + self.target = target + + def setdefault(self, key, default): + if not hasattr(self.target, key): + setattr(self.target, key, default) + return getattr(self.target, key) + + +# Sphinx begin config.environments +Config.environments = environments = { + 'staging': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + }, + 'production': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + }, + 'embedded': { + # For use with CherryPy embedded in another deployment stack. + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }, + 'test_suite': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': True, + 'request.show_mismatched_params': True, + 'log.screen': False, + }, +} +# Sphinx end config.environments + + +def _server_namespace_handler(k, v): + """Config handler for the "server" namespace.""" + atoms = k.split('.', 1) + if len(atoms) > 1: + # Special-case config keys of the form 'server.servername.socket_port' + # to configure additional HTTP servers. + if not hasattr(cherrypy, 'servers'): + cherrypy.servers = {} + + servername, k = atoms + if servername not in cherrypy.servers: + from cherrypy import _cpserver + cherrypy.servers[servername] = _cpserver.Server() + # On by default, but 'on = False' can unsubscribe it (see below). + cherrypy.servers[servername].subscribe() + + if k == 'on': + if v: + cherrypy.servers[servername].subscribe() + else: + cherrypy.servers[servername].unsubscribe() + else: + setattr(cherrypy.servers[servername], k, v) + else: + setattr(cherrypy.server, k, v) + + +Config.namespaces['server'] = _server_namespace_handler + + +def _engine_namespace_handler(k, v): + """Config handler for the "engine" namespace.""" + engine = cherrypy.engine + + if k in {'SIGHUP', 'SIGTERM'}: + engine.subscribe(k, v) + return + + if '.' in k: + plugin, attrname = k.split('.', 1) + plugin = getattr(engine, plugin) + op = 'subscribe' if v else 'unsubscribe' + sub_unsub = getattr(plugin, op, None) + if attrname == 'on' and callable(sub_unsub): + sub_unsub() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) + + +Config.namespaces['engine'] = _engine_namespace_handler + + +def _tree_namespace_handler(k, v): + """Namespace handler for the 'tree' config namespace.""" + if isinstance(v, dict): + for script_name, app in v.items(): + cherrypy.tree.graft(app, script_name) + msg = 'Mounted: %s on %s' % (app, script_name or '/') + cherrypy.engine.log(msg) + else: + cherrypy.tree.graft(v, v.script_name) + cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/')) + + +Config.namespaces['tree'] = _tree_namespace_handler diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpdispatch.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpdispatch.py new file mode 100644 index 000000000..5c506e997 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpdispatch.py @@ -0,0 +1,682 @@ +"""CherryPy dispatchers. + +A 'dispatcher' is the object which looks up the 'page handler' callable +and collects config for the current request based on the path_info, other +request attributes, and the application architecture. The core calls the +dispatcher as early as possible, passing it a 'path_info' argument. + +The default dispatcher discovers the page handler by matching path_info +to a hierarchical arrangement of objects, starting at request.app.root. +""" + +import string +import sys +import types +try: + classtype = (type, types.ClassType) +except AttributeError: + classtype = type + +import cherrypy + + +class PageHandler(object): + + """Callable which sets response.body.""" + + def __init__(self, callable, *args, **kwargs): + self.callable = callable + self.args = args + self.kwargs = kwargs + + @property + def args(self): + """The ordered args should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.args + + @args.setter + def args(self, args): + cherrypy.serving.request.args = args + return cherrypy.serving.request.args + + @property + def kwargs(self): + """The named kwargs should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + return cherrypy.serving.request.kwargs + + def __call__(self): + try: + return self.callable(*self.args, **self.kwargs) + except TypeError: + x = sys.exc_info()[1] + try: + test_callable_spec(self.callable, self.args, self.kwargs) + except cherrypy.HTTPError: + raise sys.exc_info()[1] + except Exception: + raise x + raise + + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the + handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + show_mismatched_params = getattr( + cherrypy.serving.request, 'show_mismatched_params', False) + try: + (args, varargs, varkw, defaults) = getargspec(callable) + except TypeError: + if isinstance(callable, object) and hasattr(callable, '__call__'): + (args, varargs, varkw, + defaults) = getargspec(callable.__call__) + else: + # If it wasn't one of our own types, re-raise + # the original error + raise + + if args and ( + # For callable objects, which have a __call__(self) method + hasattr(callable, '__call__') or + # For normal methods + inspect.ismethod(callable) + ): + # Strip 'self' + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + # figure out which args have defaults. + args_with_defaults = args[-len(defaults or []):] + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args_with_defaults[i]] == 0: + arg_usage[args_with_defaults[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.items(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + message = None + if show_mismatched_params: + message = 'Missing parameters: %s' % ','.join(missing_args) + raise cherrypy.HTTPError(404, message=message) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.serving.request.body.params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + message = None + if show_mismatched_params: + message = 'Multiple values for parameters: '\ + '%s' % ','.join(multiple_args) + raise cherrypy.HTTPError(error, message=message) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + message = None + if show_mismatched_params: + message = 'Unexpected query string '\ + 'parameters: %s' % ', '.join(extra_qs_params) + raise cherrypy.HTTPError(404, message=message) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + message = None + if show_mismatched_params: + message = 'Unexpected body parameters: '\ + '%s' % ', '.join(extra_body_params) + raise cherrypy.HTTPError(400, message=message) + + +try: + import inspect +except ImportError: + def test_callable_spec(callable, args, kwargs): # noqa: F811 + return None +else: + def getargspec(callable): + return inspect.getfullargspec(callable)[:4] + + +class LateParamPageHandler(PageHandler): + + """When passing cherrypy.request.params to the page handler, we do not + want to capture that dict too early; we want to give tools like the + decoding tool a chance to modify the params dict in-between the lookup + of the handler and the actual calling of the handler. This subclass + takes that into account, and allows request.params to be 'bound late' + (it's more complicated than that, but that's the effect). + """ + + @property + def kwargs(self): + """Page handler kwargs (with cherrypy.request.params copied in).""" + kwargs = cherrypy.serving.request.params.copy() + if self._kwargs: + kwargs.update(self._kwargs) + return kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + self._kwargs = kwargs + + +if sys.version_info < (3, 0): + punctuation_to_underscores = string.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, str) or len(t) != 256: + raise ValueError( + 'The translate argument must be a str of len 256.') +else: + punctuation_to_underscores = str.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, dict): + raise ValueError('The translate argument must be a dict.') + + +class Dispatcher(object): + + """CherryPy Dispatcher which walks a tree of objects to find a handler. + + The tree is rooted at cherrypy.request.app.root, and each hierarchical + component in the path_info argument is matched to a corresponding nested + attribute of the root object. Matching handlers must have an 'exposed' + attribute which evaluates to True. The special method name "index" + matches a URI which ends in a slash ("/"). The special method name + "default" may match a portion of the path_info (but only when no longer + substring of the path_info matches some other object). + + This is the default, built-in dispatcher for CherryPy. + """ + + dispatch_method_name = '_cp_dispatch' + """ + The name of the dispatch method that nodes may optionally implement + to provide their own dynamic dispatch algorithm. + """ + + def __init__(self, dispatch_method_name=None, + translate=punctuation_to_underscores): + validate_translator(translate) + self.translate = translate + if dispatch_method_name: + self.dispatch_method_name = dispatch_method_name + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + func, vpath = self.find_handler(path_info) + + if func: + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.NotFound() + + def find_handler(self, path): + """Return the appropriate page handler, plus any virtual path. + + This will return two objects. The first will be a callable, + which can be used to generate page output. Any parameters from + the query string or request body will be sent to that callable + as keyword arguments. + + The callable is found by traversing the application's tree, + starting from cherrypy.request.app.root, and matching path + components to successive objects in the tree. For example, the + URL "/path/to/handler" might return root.path.to.handler. + + The second object returned will be a list of names which are + 'virtual path' components: parts of the URL which are dynamic, + and were not used when looking up the handler. + These virtual path components are passed to the handler as + positional arguments. + """ + request = cherrypy.serving.request + app = request.app + root = app.root + dispatch_name = self.dispatch_method_name + + # Get config for the root object/path. + fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] + fullpath_len = len(fullpath) + segleft = fullpath_len + nodeconf = {} + if hasattr(root, '_cp_config'): + nodeconf.update(root._cp_config) + if '/' in app.config: + nodeconf.update(app.config['/']) + object_trail = [['root', root, nodeconf, segleft]] + + node = root + iternames = fullpath[:] + while iternames: + name = iternames[0] + # map to legal Python identifiers (e.g. replace '.' with '_') + objname = name.translate(self.translate) + + nodeconf = {} + subnode = getattr(node, objname, None) + pre_len = len(iternames) + if subnode is None: + dispatch = getattr(node, dispatch_name, None) + if dispatch and hasattr(dispatch, '__call__') and not \ + getattr(dispatch, 'exposed', False) and \ + pre_len > 1: + # Don't expose the hidden 'index' token to _cp_dispatch + # We skip this if pre_len == 1 since it makes no sense + # to call a dispatcher when we have no tokens left. + index_name = iternames.pop() + subnode = dispatch(vpath=iternames) + iternames.append(index_name) + else: + # We didn't find a path, but keep processing in case there + # is a default() handler. + iternames.pop(0) + else: + # We found the path, remove the vpath entry + iternames.pop(0) + segleft = len(iternames) + if segleft > pre_len: + # No path segment was removed. Raise an error. + raise cherrypy.CherryPyException( + 'A vpath segment was added. Custom dispatchers may only ' + 'remove elements. While trying to process ' + '{0} in {1}'.format(name, fullpath) + ) + elif segleft == pre_len: + # Assume that the handler used the current path segment, but + # did not pop it. This allows things like + # return getattr(self, vpath[0], None) + iternames.pop(0) + segleft -= 1 + node = subnode + + if node is not None: + # Get _cp_config attached to this node. + if hasattr(node, '_cp_config'): + nodeconf.update(node._cp_config) + + # Mix in values from app.config for this path. + existing_len = fullpath_len - pre_len + if existing_len != 0: + curpath = '/' + '/'.join(fullpath[0:existing_len]) + else: + curpath = '' + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] + for seg in new_segs: + curpath += '/' + seg + if curpath in app.config: + nodeconf.update(app.config[curpath]) + + object_trail.append([name, node, nodeconf, segleft]) + + def set_conf(): + """Collapse all object_trail config into cherrypy.request.config. + """ + base = cherrypy.config.copy() + # Note that we merge the config from each node + # even if that node was None. + for name, obj, conf, segleft in object_trail: + base.update(conf) + if 'tools.staticdir.dir' in conf: + base['tools.staticdir.section'] = '/' + \ + '/'.join(fullpath[0:fullpath_len - segleft]) + return base + + # Try successive objects (reverse order) + num_candidates = len(object_trail) - 1 + for i in range(num_candidates, -1, -1): + + name, candidate, nodeconf, segleft = object_trail[i] + if candidate is None: + continue + + # Try a "default" method on the current leaf. + if hasattr(candidate, 'default'): + defhandler = candidate.default + if getattr(defhandler, 'exposed', False): + # Insert any extra _cp_config from the default handler. + conf = getattr(defhandler, '_cp_config', {}) + object_trail.insert( + i + 1, ['default', defhandler, conf, segleft]) + request.config = set_conf() + # See https://github.com/cherrypy/cherrypy/issues/613 + request.is_index = path.endswith('/') + return defhandler, fullpath[fullpath_len - segleft:-1] + + # Uncomment the next line to restrict positional params to + # "default". + # if i < num_candidates - 2: continue + + # Try the current leaf. + if getattr(candidate, 'exposed', False): + request.config = set_conf() + if i == num_candidates: + # We found the extra ".index". Mark request so tools + # can redirect if path_info has no trailing slash. + request.is_index = True + else: + # We're not at an 'index' handler. Mark request so tools + # can redirect if path_info has NO trailing slash. + # Note that this also includes handlers which take + # positional parameters (virtual paths). + request.is_index = False + return candidate, fullpath[fullpath_len - segleft:-1] + + # We didn't find anything + request.config = set_conf() + return None, [] + + +class MethodDispatcher(Dispatcher): + + """Additional dispatch based on cherrypy.request.method.upper(). + + Methods named GET, POST, etc will be called on an exposed class. + The method names must be all caps; the appropriate Allow header + will be output showing all capitalized method names as allowable + HTTP verbs. + + Note that the containing class must be exposed, not the methods. + """ + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + resource, vpath = self.find_handler(path_info) + + if resource: + # Set Allow header + avail = [m for m in dir(resource) if m.isupper()] + if 'GET' in avail and 'HEAD' not in avail: + avail.append('HEAD') + avail.sort() + cherrypy.serving.response.headers['Allow'] = ', '.join(avail) + + # Find the subhandler + meth = request.method.upper() + func = getattr(resource, meth, None) + if func is None and meth == 'HEAD': + func = getattr(resource, 'GET', None) + if func: + # Grab any _cp_config on the subhandler. + if hasattr(func, '_cp_config'): + request.config.update(func._cp_config) + + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.HTTPError(405) + else: + request.handler = cherrypy.NotFound() + + +class RoutesDispatcher(object): + + """A Routes based dispatcher for CherryPy.""" + + def __init__(self, full_result=False, **mapper_options): + """ + Routes dispatcher + + Set full_result to True if you wish the controller + and the action to be passed on to the page handler + parameters. By default they won't be. + """ + import routes + self.full_result = full_result + self.controllers = {} + self.mapper = routes.Mapper(**mapper_options) + self.mapper.controller_scan = self.controllers.keys + + def connect(self, name, route, controller, **kwargs): + self.controllers[name] = controller + self.mapper.connect(name, route, controller=name, **kwargs) + + def redirect(self, url): + raise cherrypy.HTTPRedirect(url) + + def __call__(self, path_info): + """Set handler and config for the current request.""" + func = self.find_handler(path_info) + if func: + cherrypy.serving.request.handler = LateParamPageHandler(func) + else: + cherrypy.serving.request.handler = cherrypy.NotFound() + + def find_handler(self, path_info): + """Find the right page handler, and set request.config.""" + import routes + + request = cherrypy.serving.request + + config = routes.request_config() + config.mapper = self.mapper + if hasattr(request, 'wsgi_environ'): + config.environ = request.wsgi_environ + config.host = request.headers.get('Host', None) + config.protocol = request.scheme + config.redirect = self.redirect + + result = self.mapper.match(path_info) + + config.mapper_dict = result + params = {} + if result: + params = result.copy() + if not self.full_result: + params.pop('controller', None) + params.pop('action', None) + request.params.update(params) + + # Get config for the root object/path. + request.config = base = cherrypy.config.copy() + curpath = '' + + def merge(nodeconf): + if 'tools.staticdir.dir' in nodeconf: + nodeconf['tools.staticdir.section'] = curpath or '/' + base.update(nodeconf) + + app = request.app + root = app.root + if hasattr(root, '_cp_config'): + merge(root._cp_config) + if '/' in app.config: + merge(app.config['/']) + + # Mix in values from app.config. + atoms = [x for x in path_info.split('/') if x] + if atoms: + last = atoms.pop() + else: + last = None + for atom in atoms: + curpath = '/'.join((curpath, atom)) + if curpath in app.config: + merge(app.config[curpath]) + + handler = None + if result: + controller = result.get('controller') + controller = self.controllers.get(controller, controller) + if controller: + if isinstance(controller, classtype): + controller = controller() + # Get config from the controller. + if hasattr(controller, '_cp_config'): + merge(controller._cp_config) + + action = result.get('action') + if action is not None: + handler = getattr(controller, action, None) + # Get config from the handler + if hasattr(handler, '_cp_config'): + merge(handler._cp_config) + else: + handler = controller + + # Do the last path atom here so it can + # override the controller's _cp_config. + if last: + curpath = '/'.join((curpath, last)) + if curpath in app.config: + merge(app.config[curpath]) + + return handler + + +def XMLRPCDispatcher(next_dispatcher=Dispatcher()): + from cherrypy.lib import xmlrpcutil + + def xmlrpc_dispatch(path_info): + path_info = xmlrpcutil.patched_path(path_info) + return next_dispatcher(path_info) + return xmlrpc_dispatch + + +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, + **domains): + """ + Select a different handler based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different parts of a single + website structure. For example:: + + http://www.domain.example -> root + http://www.domain2.example -> root/domain2/ + http://www.domain2.example:443 -> root/secure + + can be accomplished via the following config:: + + [/] + request.dispatch = cherrypy.dispatch.VirtualHost( + **{'www.domain2.example': '/domain2', + 'www.domain2.example:443': '/secure', + }) + + next_dispatcher + The next dispatcher object in the dispatch chain. + The VirtualHost dispatcher adds a prefix to the URL and calls + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). + + use_x_forwarded_host + If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying. + + ``**domains`` + A dict of {host header value: virtual prefix} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding "virtual prefix" + value will be prepended to the URL path before calling the + next dispatcher. Note that you often need separate entries + for "example.com" and "www.example.com". In addition, "Host" + headers may contain the port number. + """ + from cherrypy.lib import httputil + + def vhost_dispatch(path_info): + request = cherrypy.serving.request + header = request.headers.get + + domain = header('Host', '') + if use_x_forwarded_host: + domain = header('X-Forwarded-Host', domain) + + prefix = domains.get(domain, '') + if prefix: + path_info = httputil.urljoin(prefix, path_info) + + result = next_dispatcher(path_info) + + # Touch up staticdir config. See + # https://github.com/cherrypy/cherrypy/issues/614. + section = request.config.get('tools.staticdir.section') + if section: + section = section[len(prefix):] + request.config['tools.staticdir.section'] = section + + return result + return vhost_dispatch diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cperror.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cperror.py new file mode 100644 index 000000000..f6ff2913f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cperror.py @@ -0,0 +1,620 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise +themselves; this means you can set an +:class:`HTTPError` +or :class:`HTTPRedirect` as the +:attr:`request.handler`. + +.. _redirectingpost: + +Redirecting POST +================ + +When you GET a resource and are redirected by the server to another Location, +there's generally no problem since GET is both a "safe method" (there should +be no side-effects) and an "idempotent method" (multiple calls are no different +than a single call). + +POST, however, is neither safe nor idempotent--if you +charge a credit card, you don't want to be charged twice by a redirect! + +For this reason, *none* of the 3xx responses permit a user-agent (browser) to +resubmit a POST on redirection without first confirming the action with the +user: + +===== ================================= =========== +300 Multiple Choices Confirm with the user +301 Moved Permanently Confirm with the user +302 Found (Object moved temporarily) Confirm with the user +303 See Other GET the new URI; no confirmation +304 Not modified for conditional GET only; + POST should not raise this error +305 Use Proxy Confirm with the user +307 Temporary Redirect Confirm with the user +308 Permanent Redirect No confirmation +===== ================================= =========== + +However, browsers have historically implemented these restrictions poorly; +in particular, many browsers do not force the user to confirm 301, 302 +or 307 when redirecting POST. For this reason, CherryPy defaults to 303, +which most user-agents appear to have implemented correctly. Therefore, if +you raise HTTPRedirect for a POST request, the user-agent will most likely +attempt to GET the new URI (without asking for confirmation from the user). +We realize this is confusing for developers, but it's the safest thing we +could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` +or any other 3xx status if you know what you're doing, but given the +environment, we couldn't let any of those be the default. + +Custom Error Handling +===================== + +.. image:: /refman/cperrors.gif + +Anticipated HTTP responses +-------------------------- + +The 'error_page' config namespace can be used to provide custom HTML output for +expected responses (like 404 Not Found). Supply a filename from which the +output will be read. The contents will be interpolated with the values +%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python +`string formatting +`_. + +:: + + _cp_config = { + 'error_page.404': os.path.join(localDir, "static/index.html") + } + + +Beginning in version 3.1, you may also provide a function or other callable as +an error_page entry. It will be passed the same status, message, traceback and +version arguments that are interpolated into templates:: + + def error_page_402(status, message, traceback, version): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + cherrypy.config.update({'error_page.402': error_page_402}) + +Also in 3.1, in addition to the numbered error codes, you may also supply +"error_page.default" to handle all codes which do not have their own error_page +entry. + + + +Unanticipated errors +-------------------- + +CherryPy also has a generic error handling mechanism: whenever an unanticipated +error occurs in your code, it will call +:func:`Request.error_response` to +set the response status, headers, and body. By default, this is the same +output as +:class:`HTTPError(500) `. If you want to provide +some other behavior, you generally replace "request.error_response". + +Here is some sample code that shows how to display a custom error message and +send an e-mail containing the error:: + + from cherrypy import _cperror + + def handle_error(): + cherrypy.response.status = 500 + cherrypy.response.body = [ + "Sorry, an error occurred" + ] + sendMail('error@domain.com', + 'Error in your web app', + _cperror.format_exc()) + + @cherrypy.config(**{'request.error_response': handle_error}) + class Root: + pass + +Note that you have to explicitly set +:attr:`response.body ` +and not simply return an error message as a result. +""" + +import io +import contextlib +import urllib.parse +from sys import exc_info as _exc_info +from traceback import format_exception as _format_exception +from xml.sax import saxutils +import html + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy._cpcompat import tonative +from cherrypy._helper import classproperty +from cherrypy.lib import httputil as _httputil + + +class CherryPyException(Exception): + + """A base class for CherryPy exceptions.""" + pass + + +class InternalRedirect(CherryPyException): + + """Exception raised to switch to the handler for a different URL. + + This exception will redirect processing to another path within the site + (without informing the client). Provide the new path as an argument when + raising the exception. Provide any params in the querystring for the new + URL. + """ + + def __init__(self, path, query_string=''): + self.request = cherrypy.serving.request + + self.query_string = query_string + if '?' in path: + # Separate any params included in the path + path, self.query_string = path.split('?', 1) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a URL relative to root (e.g. "/dummy") + # 2. a URL relative to the current path + # Note that any query string will be discarded. + path = urllib.parse.urljoin(self.request.path_info, path) + + # Set a 'path' member attribute so that code which traps this + # error can have access to it. + self.path = path + + CherryPyException.__init__(self, path, self.query_string) + + +class HTTPRedirect(CherryPyException): + + """Exception raised when the request should be redirected. + + This exception will force a HTTP redirect to the URL or URL's you give it. + The new URL must be passed as the first argument to the Exception, + e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. + If a URL is absolute, it will be used as-is. If it is relative, it is + assumed to be relative to the current cherrypy.request.path_info. + + If one of the provided URL is a unicode object, it will be encoded + using the default encoding or the one passed in parameter. + + There are multiple types of redirect, from which you can select via the + ``status`` argument. If you do not provide a ``status`` arg, it defaults to + 303 (or 302 if responding with HTTP/1.0). + + Examples:: + + raise cherrypy.HTTPRedirect("") + raise cherrypy.HTTPRedirect("/abs/path", 307) + raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) + + See :ref:`redirectingpost` for additional caveats. + """ + + urls = None + """The list of URL's to emit.""" + + encoding = 'utf-8' + """The encoding when passed urls are not native strings""" + + def __init__(self, urls, status=None, encoding=None): + self.urls = abs_urls = [ + # Note that urljoin will "do the right thing" whether url is: + # 1. a complete URL with host (e.g. "http://www.example.com/test") + # 2. a URL relative to root (e.g. "/dummy") + # 3. a URL relative to the current path + # Note that any query string in cherrypy.request is discarded. + urllib.parse.urljoin( + cherrypy.url(), + tonative(url, encoding or self.encoding), + ) + for url in always_iterable(urls) + ] + + status = ( + int(status) + if status is not None + else self.default_status + ) + if not 300 <= status <= 399: + raise ValueError('status must be between 300 and 399.') + + CherryPyException.__init__(self, abs_urls, status) + + @classproperty + def default_status(cls): + """ + The default redirect status for the request. + + RFC 2616 indicates a 301 response code fits our goal; however, + browser support for 301 is quite messy. Use 302/303 instead. See + http://www.alanflavell.org.uk/www/post-redirect.html + """ + return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302 + + @property + def status(self): + """The integer HTTP status code to emit.""" + _, status = self.args[:2] + return status + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPRedirect object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + response.status = status = self.status + + if status in (300, 301, 302, 303, 307, 308): + response.headers['Content-Type'] = 'text/html;charset=utf-8' + # "The ... URI SHOULD be given by the Location field + # in the response." + response.headers['Location'] = self.urls[0] + + # "Unless the request method was HEAD, the entity of the response + # SHOULD contain a short hypertext note with a hyperlink to the + # new URI(s)." + msg = { + 300: 'This resource can be found at ', + 301: 'This resource has permanently moved to ', + 302: 'This resource resides temporarily at ', + 303: 'This resource can be found at ', + 307: 'This resource has moved temporarily to ', + 308: 'This resource has been moved to ', + }[status] + msg += '%s.' + msgs = [ + msg % (saxutils.quoteattr(u), html.escape(u, quote=False)) + for u in self.urls + ] + response.body = ntob('
\n'.join(msgs), 'utf-8') + # Previous code may have set C-L, so we have to reset it + # (allow finalize to set it). + response.headers.pop('Content-Length', None) + elif status == 304: + # Not Modified. + # "The response MUST include the following header fields: + # Date, unless its omission is required by section 14.18.1" + # The "Date" header should have been set in Response.__init__ + + # "...the response SHOULD NOT include other entity-headers." + for key in ('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Location', 'Content-MD5', + 'Content-Range', 'Content-Type', 'Expires', + 'Last-Modified'): + if key in response.headers: + del response.headers[key] + + # "The 304 response MUST NOT contain a message-body." + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + elif status == 305: + # Use Proxy. + # self.urls[0] should be the URI of the proxy. + response.headers['Location'] = ntob(self.urls[0], 'utf-8') + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + else: + raise ValueError('The %s status code is unknown.' % status) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +def clean_headers(status): + """Remove any headers which should not apply to an error response.""" + response = cherrypy.serving.response + + # Remove headers which applied to the original content, + # but do not apply to the error page. + respheaders = response.headers + for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After', + 'Vary', 'Content-Encoding', 'Content-Length', 'Expires', + 'Content-Location', 'Content-MD5', 'Last-Modified']: + if key in respheaders: + del respheaders[key] + + if status != 416: + # A server sending a response with status code 416 (Requested + # range not satisfiable) SHOULD include a Content-Range field + # with a byte-range-resp-spec of "*". The instance-length + # specifies the current length of the selected resource. + # A response with status code 206 (Partial Content) MUST NOT + # include a Content-Range field with a byte-range- resp-spec of "*". + if 'Content-Range' in respheaders: + del respheaders['Content-Range'] + + +class HTTPError(CherryPyException): + + """Exception used to return an HTTP error code (4xx-5xx) to the client. + + This exception can be used to automatically send a response using a + http status code, with an appropriate error page. It takes an optional + ``status`` argument (which must be between 400 and 599); it defaults to 500 + ("Internal Server Error"). It also takes an optional ``message`` argument, + which will be returned in the response body. See + `RFC2616 `_ + for a complete list of available error codes and when to use them. + + Examples:: + + raise cherrypy.HTTPError(403) + raise cherrypy.HTTPError( + "403 Forbidden", "You are not allowed to access this resource.") + """ + + status = None + """The HTTP status code. May be of type int or str (with a Reason-Phrase). + """ + + code = None + """The integer HTTP status code.""" + + reason = None + """The HTTP Reason-Phrase string.""" + + def __init__(self, status=500, message=None): + self.status = status + try: + self.code, self.reason, defaultmsg = _httputil.valid_status(status) + except ValueError: + raise self.__class__(500, _exc_info()[1].args[0]) + + if self.code < 400 or self.code > 599: + raise ValueError('status must be between 400 and 599.') + + # See http://www.python.org/dev/peps/pep-0352/ + # self.message = message + self._message = message or defaultmsg + CherryPyException.__init__(self, status, message) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPError object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + + clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + tb = None + if cherrypy.serving.request.show_tracebacks: + tb = format_exc() + + response.headers.pop('Content-Length', None) + + content = self.get_error_page(self.status, traceback=tb, + message=self._message) + response.body = content + + _be_ie_unfriendly(self.code) + + def get_error_page(self, *args, **kwargs): + return get_error_page(*args, **kwargs) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + @classmethod + @contextlib.contextmanager + def handle(cls, exception, status=500, message=''): + """Translate exception into an HTTPError.""" + try: + yield + except exception as exc: + raise cls(status, message or str(exc)) + + +class NotFound(HTTPError): + + """Exception raised when a URL could not be mapped to any handler (404). + + This is equivalent to raising + :class:`HTTPError("404 Not Found") `. + """ + + def __init__(self, path=None): + if path is None: + request = cherrypy.serving.request + path = request.script_name + request.path_info + self.args = (path,) + HTTPError.__init__(self, 404, "The path '%s' was not found." % path) + + +_HTTPErrorTemplate = ''' + + + + %(status)s + + + +

%(status)s

+

%(message)s

+
%(traceback)s
+
+ + Powered by CherryPy %(version)s + +
+ + +''' + + +def get_error_page(status, **kwargs): + """Return an HTML page, containing a pretty error response. + + status should be an int or a str. + kwargs will be interpolated into the page template. + """ + try: + code, reason, message = _httputil.valid_status(status) + except ValueError: + raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) + + # We can't use setdefault here, because some + # callers send None for kwarg values. + if kwargs.get('status') is None: + kwargs['status'] = '%s %s' % (code, reason) + if kwargs.get('message') is None: + kwargs['message'] = message + if kwargs.get('traceback') is None: + kwargs['traceback'] = '' + if kwargs.get('version') is None: + kwargs['version'] = cherrypy.__version__ + + for k, v in kwargs.items(): + if v is None: + kwargs[k] = '' + else: + kwargs[k] = html.escape(kwargs[k], quote=False) + + # Use a custom template or callable for the error page? + pages = cherrypy.serving.request.error_page + error_page = pages.get(code) or pages.get('default') + + # Default template, can be overridden below. + template = _HTTPErrorTemplate + if error_page: + try: + if hasattr(error_page, '__call__'): + # The caller function may be setting headers manually, + # so we delegate to it completely. We may be returning + # an iterator as well as a string here. + # + # We *must* make sure any content is not unicode. + result = error_page(**kwargs) + if cherrypy.lib.is_iterator(result): + from cherrypy.lib.encoding import UTF8StreamEncoder + return UTF8StreamEncoder(result) + elif isinstance(result, str): + return result.encode('utf-8') + else: + if not isinstance(result, bytes): + raise ValueError( + 'error page function did not ' + 'return a bytestring, str or an ' + 'iterator - returned object of type %s.' + % (type(result).__name__)) + return result + else: + # Load the template from this path. + with io.open(error_page, newline='') as f: + template = f.read() + except Exception: + e = _format_exception(*_exc_info())[-1] + m = kwargs['message'] + if m: + m += '
' + m += 'In addition, the custom error page failed:\n
%s' % e + kwargs['message'] = m + + response = cherrypy.serving.response + response.headers['Content-Type'] = 'text/html;charset=utf-8' + result = template % kwargs + return result.encode('utf-8') + + +_ie_friendly_error_sizes = { + 400: 512, 403: 256, 404: 512, 405: 256, + 406: 512, 408: 512, 409: 512, 410: 256, + 500: 512, 501: 512, 505: 512, +} + + +def _be_ie_unfriendly(status): + response = cherrypy.serving.response + + # For some statuses, Internet Explorer 5+ shows "friendly error + # messages" instead of our response.body if the body is smaller + # than a given size. Fix this by returning a body over that size + # (by adding whitespace). + # See http://support.microsoft.com/kb/q218155/ + s = _ie_friendly_error_sizes.get(status, 0) + if s: + s += 1 + # Since we are issuing an HTTP error status, we assume that + # the entity is short, and we should just collapse it. + content = response.collapse_body() + content_length = len(content) + if content_length and content_length < s: + # IN ADDITION: the response must be written to IE + # in one chunk or it will still get replaced! Bah. + content = content + (b' ' * (s - content_length)) + response.body = content + response.headers['Content-Length'] = str(len(content)) + + +def format_exc(exc=None): + """Return exc (or sys.exc_info if None), formatted.""" + try: + if exc is None: + exc = _exc_info() + if exc == (None, None, None): + return '' + import traceback + return ''.join(traceback.format_exception(*exc)) + finally: + del exc + + +def bare_error(extrabody=None): + """Produce status, headers, body for a critical error. + + Returns a triple without calling any other questionable functions, + so it should be as error-free as possible. Call it from an HTTP server + if you get errors outside of the request. + + If extrabody is None, a friendly but rather unhelpful error message + is set in the body. If extrabody is a string, it will be appended + as-is to the body. + """ + + # The whole point of this function is to be a last line-of-defense + # in handling errors. That is, it must not raise any errors itself; + # it cannot be allowed to fail. Therefore, don't add to it! + # In particular, don't call any other CP functions. + + body = b'Unrecoverable error in the server.' + if extrabody is not None: + if not isinstance(extrabody, bytes): + extrabody = extrabody.encode('utf-8') + body += b'\n' + extrabody + + return (b'500 Internal Server Error', + [(b'Content-Type', b'text/plain'), + (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))], + [body]) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cplogging.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cplogging.py new file mode 100644 index 000000000..bce1c87bf --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cplogging.py @@ -0,0 +1,457 @@ +""" +Simple config +============= + +Although CherryPy uses the :mod:`Python logging module `, it does so +behind the scenes so that simple logging is simple, but complicated logging +is still possible. "Simple" logging means that you can log to the screen +(i.e. console/stdout) or to a file, and that you can easily have separate +error and access log files. + +Here are the simplified logging settings. You use these by adding lines to +your config file or dict. You should set these at either the global level or +per application (see next), but generally not both. + + * ``log.screen``: Set this to True to have both "error" and "access" messages + printed to stdout. + * ``log.access_file``: Set this to an absolute filename where you want + "access" messages written. + * ``log.error_file``: Set this to an absolute filename where you want "error" + messages written. + +Many events are automatically logged; to log your own application events, call +:func:`cherrypy.log`. + +Architecture +============ + +Separate scopes +--------------- + +CherryPy provides log managers at both the global and application layers. +This means you can have one set of logging rules for your entire site, +and another set of rules specific to each application. The global log +manager is found at :func:`cherrypy.log`, and the log manager for each +application is found at :attr:`app.log`. +If you're inside a request, the latter is reachable from +``cherrypy.request.app.log``; if you're outside a request, you'll have to +obtain a reference to the ``app``: either the return value of +:func:`tree.mount()` or, if you used +:func:`quickstart()` instead, via +``cherrypy.tree.apps['/']``. + +By default, the global logs are named "cherrypy.error" and "cherrypy.access", +and the application logs are named "cherrypy.error.2378745" and +"cherrypy.access.2378745" (the number is the id of the Application object). +This means that the application logs "bubble up" to the site logs, so if your +application has no log handlers, the site-level handlers will still log the +messages. + +Errors vs. Access +----------------- + +Each log manager handles both "access" messages (one per HTTP request) and +"error" messages (everything else). Note that the "error" log is not just for +errors! The format of access messages is highly formalized, but the error log +isn't--it receives messages from a variety of sources (including full error +tracebacks, if enabled). + +If you are logging the access log and error log to the same source, then there +is a possibility that a specially crafted error message may replicate an access +log message as described in CWE-117. In this case it is the application +developer's responsibility to manually escape data before +using CherryPy's log() +functionality, or they may create an application that is vulnerable to CWE-117. +This would be achieved by using a custom handler escape any special characters, +and attached as described below. + +Custom Handlers +=============== + +The simple settings above work by manipulating Python's standard :mod:`logging` +module. So when you need something more complex, the full power of the standard +module is yours to exploit. You can borrow or create custom handlers, formats, +filters, and much more. Here's an example that skips the standard FileHandler +and uses a RotatingFileHandler instead: + +:: + + #python + log = app.log + + # Remove the default FileHandlers if present. + log.error_file = "" + log.access_file = "" + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + fname = getattr(log, "rot_error_file", "error.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + fname = getattr(log, "rot_access_file", "access.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.access_log.addHandler(h) + + +The ``rot_*`` attributes are pulled straight from the application log object. +Since "log.*" config entries simply set attributes on the log object, you can +add custom attributes to your heart's content. Note that these handlers are +used ''instead'' of the default, simple handlers outlined above (so don't set +the "log.error_file" config entry, for example). +""" + +import datetime +import logging +import os +import sys + +import cherrypy +from cherrypy import _cperror + + +# Silence the no-handlers "warning" (stderr write!) in stdlib logging +logging.Logger.manager.emittedNoHandlerWarning = 1 +logfmt = logging.Formatter('%(message)s') + + +class NullHandler(logging.Handler): + + """A no-op logging handler to silence the logging.lastResort handler.""" + + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + +class LogManager(object): + + """An object to assist both simple and advanced logging. + + ``cherrypy.log`` is an instance of this class. + """ + + appid = None + """The id() of the Application object which owns this log manager. If this + is a global log manager, appid is None.""" + + error_log = None + """The actual :class:`logging.Logger` instance for error messages.""" + + access_log = None + """The actual :class:`logging.Logger` instance for access messages.""" + + access_log_format = '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' + + logger_root = None + """The "top-level" logger name. + + This string will be used as the first segment in the Logger names. + The default is "cherrypy", for example, in which case the Logger names + will be of the form:: + + cherrypy.error. + cherrypy.access. + """ + + def __init__(self, appid=None, logger_root='cherrypy'): + self.logger_root = logger_root + self.appid = appid + if appid is None: + self.error_log = logging.getLogger('%s.error' % logger_root) + self.access_log = logging.getLogger('%s.access' % logger_root) + else: + self.error_log = logging.getLogger( + '%s.error.%s' % (logger_root, appid)) + self.access_log = logging.getLogger( + '%s.access.%s' % (logger_root, appid)) + self.error_log.setLevel(logging.INFO) + self.access_log.setLevel(logging.INFO) + + # Silence the no-handlers "warning" (stderr write!) in stdlib logging + self.error_log.addHandler(NullHandler()) + self.access_log.addHandler(NullHandler()) + + cherrypy.engine.subscribe('graceful', self.reopen_files) + + def reopen_files(self): + """Close and reopen all file handlers.""" + for log in (self.error_log, self.access_log): + for h in log.handlers: + if isinstance(h, logging.FileHandler): + h.acquire() + h.stream.close() + h.stream = open(h.baseFilename, h.mode) + h.release() + + def error(self, msg='', context='', severity=logging.INFO, + traceback=False): + """Write the given ``msg`` to the error log. + + This is not just for errors! Applications may call this at any time + to log application-specific information. + + If ``traceback`` is True, the traceback of the current exception + (if any) will be appended to ``msg``. + """ + exc_info = None + if traceback: + exc_info = _cperror._exc_info() + + self.error_log.log( + severity, + ' '.join((self.time(), context, msg)), + exc_info=exc_info, + ) + + def __call__(self, *args, **kwargs): + """An alias for ``error``.""" + return self.error(*args, **kwargs) + + def access(self): + """Write to the access log (in Apache/NCSA Combined Log format). + + See the + `apache documentation + `_ + for format details. + + CherryPy calls this automatically for you. Note there are no arguments; + it collects the data itself from + :class:`cherrypy.request`. + + Like Apache started doing in 2.0.46, non-printable and other special + characters in %r (and we expand that to all parts) are escaped using + \\xhh sequences, where hh stands for the hexadecimal representation + of the raw byte. Exceptions from this rule are " and \\, which are + escaped by prepending a backslash, and all whitespace characters, + which are written in their C-style notation (\\n, \\t, etc). + """ + request = cherrypy.serving.request + remote = request.remote + response = cherrypy.serving.response + outheaders = response.headers + inheaders = request.headers + if response.output_status is None: + status = '-' + else: + status = response.output_status.split(b' ', 1)[0] + status = status.decode('ISO-8859-1') + + atoms = {'h': remote.name or remote.ip, + 'l': '-', + 'u': getattr(request, 'login', None) or '-', + 't': self.time(), + 'r': request.request_line, + 's': status, + 'b': dict.get(outheaders, 'Content-Length', '') or '-', + 'f': dict.get(inheaders, 'Referer', ''), + 'a': dict.get(inheaders, 'User-Agent', ''), + 'o': dict.get(inheaders, 'Host', '-'), + 'i': request.unique_id, + 'z': LazyRfc3339UtcTime(), + } + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v + + try: + self.access_log.log( + logging.INFO, self.access_log_format.format(**atoms)) + except Exception: + self(traceback=True) + + def time(self): + """Return now() in Apache Common Log Format (no timezone).""" + now = datetime.datetime.now() + monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + month = monthnames[now.month - 1].capitalize() + return ('[%02d/%s/%04d:%02d:%02d:%02d]' % + (now.day, month, now.year, now.hour, now.minute, now.second)) + + def _get_builtin_handler(self, log, key): + for h in log.handlers: + if getattr(h, '_cpbuiltin', None) == key: + return h + + # ------------------------- Screen handlers ------------------------- # + def _set_screen_handler(self, log, enable, stream=None): + h = self._get_builtin_handler(log, 'screen') + if enable: + if not h: + if stream is None: + stream = sys.stderr + h = logging.StreamHandler(stream) + h.setFormatter(logfmt) + h._cpbuiltin = 'screen' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def screen(self): + """Turn stderr/stdout logging on or off. + + If you set this to True, it'll add the appropriate StreamHandler for + you. If you set it to False, it will remove the handler. + """ + h = self._get_builtin_handler + has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') + return bool(has_h) + + @screen.setter + def screen(self, newvalue): + self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) + self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) + + # -------------------------- File handlers -------------------------- # + + def _add_builtin_file_handler(self, log, fname): + h = logging.FileHandler(fname) + h.setFormatter(logfmt) + h._cpbuiltin = 'file' + log.addHandler(h) + + def _set_file_handler(self, log, filename): + h = self._get_builtin_handler(log, 'file') + if filename: + if h: + if h.baseFilename != os.path.abspath(filename): + h.close() + log.handlers.remove(h) + self._add_builtin_file_handler(log, filename) + else: + self._add_builtin_file_handler(log, filename) + else: + if h: + h.close() + log.handlers.remove(h) + + @property + def error_file(self): + """The filename for self.error_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.error_log, 'file') + if h: + return h.baseFilename + return '' + + @error_file.setter + def error_file(self, newvalue): + self._set_file_handler(self.error_log, newvalue) + + @property + def access_file(self): + """The filename for self.access_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.access_log, 'file') + if h: + return h.baseFilename + return '' + + @access_file.setter + def access_file(self, newvalue): + self._set_file_handler(self.access_log, newvalue) + + # ------------------------- WSGI handlers ------------------------- # + + def _set_wsgi_handler(self, log, enable): + h = self._get_builtin_handler(log, 'wsgi') + if enable: + if not h: + h = WSGIErrorHandler() + h.setFormatter(logfmt) + h._cpbuiltin = 'wsgi' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def wsgi(self): + """Write errors to wsgi.errors. + + If you set this to True, it'll add the appropriate + :class:`WSGIErrorHandler` for you + (which writes errors to ``wsgi.errors``). + If you set it to False, it will remove the handler. + """ + return bool(self._get_builtin_handler(self.error_log, 'wsgi')) + + @wsgi.setter + def wsgi(self, newvalue): + self._set_wsgi_handler(self.error_log, newvalue) + + +class WSGIErrorHandler(logging.Handler): + + "A handler class which writes logging records to environ['wsgi.errors']." + + def flush(self): + """Flushes the stream.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + stream.flush() + + def emit(self, record): + """Emit a record.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + try: + msg = self.format(record) + fs = '%s\n' + import types + # if no unicode support... + if not hasattr(types, 'UnicodeType'): + stream.write(fs % msg) + else: + try: + stream.write(fs % msg) + except UnicodeError: + stream.write(fs % msg.encode('UTF-8')) + self.flush() + except Exception: + self.handleError(record) + + +class LazyRfc3339UtcTime(object): + def __str__(self): + """Return utcnow() in RFC3339 UTC Format.""" + iso_formatted_now = datetime.datetime.utcnow().isoformat('T') + return f'{iso_formatted_now!s}Z' diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpmodpy.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpmodpy.py new file mode 100644 index 000000000..a08f0ed9a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpmodpy.py @@ -0,0 +1,351 @@ +"""Native adapter for serving CherryPy via mod_python + +Basic usage: + +########################################## +# Application in a module called myapp.py +########################################## + +import cherrypy + +class Root: + @cherrypy.expose + def index(self): + return 'Hi there, Ho there, Hey there' + + +# We will use this method from the mod_python configuration +# as the entry point to our application +def setup_server(): + cherrypy.tree.mount(Root()) + cherrypy.config.update({'environment': 'production', + 'log.screen': False, + 'show_tracebacks': False}) + +########################################## +# mod_python settings for apache2 +# This should reside in your httpd.conf +# or a file that will be loaded at +# apache startup +########################################## + +# Start +DocumentRoot "/" +Listen 8080 +LoadModule python_module /usr/lib/apache2/modules/mod_python.so + + + PythonPath "sys.path+['/path/to/my/application']" + SetHandler python-program + PythonHandler cherrypy._cpmodpy::handler + PythonOption cherrypy.setup myapp::setup_server + PythonDebug On + +# End + +The actual path to your mod_python.so is dependent on your +environment. In this case we suppose a global mod_python +installation on a Linux distribution such as Ubuntu. + +We do set the PythonPath configuration setting so that +your application can be found by from the user running +the apache2 instance. Of course if your application +resides in the global site-package this won't be needed. + +Then restart apache2 and access http://127.0.0.1:8080 +""" + +import io +import logging +import os +import re +import sys + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +# ------------------------------ Request-handling + + +def setup(req): + from mod_python import apache + + # Run any setup functions defined by a "PythonOption cherrypy.setup" + # directive. + options = req.get_options() + if 'cherrypy.setup' in options: + for function in options['cherrypy.setup'].split(): + atoms = function.split('::', 1) + if len(atoms) == 1: + mod = __import__(atoms[0], globals(), locals()) + else: + modname, fname = atoms + mod = __import__(modname, globals(), locals(), [fname]) + func = getattr(mod, fname) + func() + + cherrypy.config.update({'log.screen': False, + 'tools.ignore_headers.on': True, + 'tools.ignore_headers.headers': ['Range'], + }) + + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.unsubscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.unsubscribe() + engine.autoreload.unsubscribe() + cherrypy.server.unsubscribe() + + @engine.subscribe('log') + def _log(msg, level): + newlevel = apache.APLOG_ERR + if logging.DEBUG >= level: + newlevel = apache.APLOG_DEBUG + elif logging.INFO >= level: + newlevel = apache.APLOG_INFO + elif logging.WARNING >= level: + newlevel = apache.APLOG_WARNING + # On Windows, req.server is required or the msg will vanish. See + # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html + # Also, "When server is not specified...LogLevel does not apply..." + apache.log_error(msg, newlevel, req.server) + + engine.start() + + def cherrypy_cleanup(data): + engine.exit() + try: + # apache.register_cleanup wasn't available until 3.1.4. + apache.register_cleanup(cherrypy_cleanup) + except AttributeError: + req.server.register_cleanup(req, cherrypy_cleanup) + + +class _ReadOnlyRequest: + expose = ('read', 'readline', 'readlines') + + def __init__(self, req): + for method in self.expose: + self.__dict__[method] = getattr(req, method) + + +recursive = False + +_isSetUp = False + + +def handler(req): + from mod_python import apache + try: + global _isSetUp + if not _isSetUp: + setup(req) + _isSetUp = True + + # Obtain a Request object from CherryPy + local = req.connection.local_addr + local = httputil.Host( + local[0], local[1], req.connection.local_host or '') + remote = req.connection.remote_addr + remote = httputil.Host( + remote[0], remote[1], req.connection.remote_host or '') + + scheme = req.parsed_uri[0] or 'http' + req.get_basic_auth_pw() + + try: + # apache.mpm_query only became available in mod_python 3.1 + q = apache.mpm_query + threaded = q(apache.AP_MPMQ_IS_THREADED) + forked = q(apache.AP_MPMQ_IS_FORKED) + except AttributeError: + bad_value = ("You must provide a PythonOption '%s', " + "either 'on' or 'off', when running a version " + 'of mod_python < 3.1') + + options = req.get_options() + + threaded = options.get('multithread', '').lower() + if threaded == 'on': + threaded = True + elif threaded == 'off': + threaded = False + else: + raise ValueError(bad_value % 'multithread') + + forked = options.get('multiprocess', '').lower() + if forked == 'on': + forked = True + elif forked == 'off': + forked = False + else: + raise ValueError(bad_value % 'multiprocess') + + sn = cherrypy.tree.script_name(req.uri or '/') + if sn is None: + send_response(req, '404 Not Found', [], '') + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.uri + qs = req.args or '' + reqproto = req.protocol + headers = list(req.headers_in.copy().items()) + rfile = _ReadOnlyRequest(req) + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving(local, remote, scheme, + 'HTTP/1.1') + request.login = req.user + request.multithread = bool(threaded) + request.multiprocess = bool(forked) + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, reqproto, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same URL ' + 'twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + send_response( + req, response.output_status, response.header_list, + response.body, response.stream) + finally: + app.release_serving() + except Exception: + tb = format_exc() + cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) + s, h, b = bare_error() + send_response(req, s, h, b) + return apache.OK + + +def send_response(req, status, headers, body, stream=False): + # Set response status + req.status = int(status[:3]) + + # Set response headers + req.content_type = 'text/plain' + for header, value in headers: + if header.lower() == 'content-type': + req.content_type = value + continue + req.headers_out.add(header, value) + + if stream: + # Flush now so the status and headers are sent immediately. + req.flush() + + # Set response body + for seg in always_iterable(body): + req.write(seg) + + +# --------------- Startup tools for CherryPy + mod_python --------------- # +try: + import subprocess + + def popen(fullcmd): + p = subprocess.Popen(fullcmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=True) + return p.stdout +except ImportError: + def popen(fullcmd): + pipein, pipeout = os.popen4(fullcmd) + return pipeout + + +def read_process(cmd, args=''): + fullcmd = '%s %s' % (cmd, args) + pipeout = popen(fullcmd) + try: + firstline = pipeout.readline() + cmd_not_found = re.search( + b'(not recognized|No such file|not found)', + firstline, + re.IGNORECASE + ) + if cmd_not_found: + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +class ModPythonServer(object): + + template = """ +# Apache2 server configuration file for running CherryPy with mod_python. + +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + + + SetHandler python-program + PythonHandler %(handler)s + PythonDebug On +%(opts)s + +""" + + def __init__(self, loc='/', port=80, opts=None, apache_path='apache', + handler='cherrypy._cpmodpy::handler'): + self.loc = loc + self.port = port + self.opts = opts + self.apache_path = apache_path + self.handler = handler + + def start(self): + opts = ''.join([' PythonOption %s %s\n' % (k, v) + for k, v in self.opts]) + conf_data = self.template % {'port': self.port, + 'loc': self.loc, + 'opts': opts, + 'handler': self.handler, + } + + mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') + with open(mpconf, 'wb') as f: + f.write(conf_data) + + response = read_process(self.apache_path, '-k start -f %s' % mpconf) + self.ready = True + return response + + def stop(self): + os.popen('apache -k stop') + self.ready = False diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpnative_server.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpnative_server.py new file mode 100644 index 000000000..e9671d289 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpnative_server.py @@ -0,0 +1,168 @@ +"""Native adapter for serving CherryPy via its builtin server.""" + +import logging +import sys +import io + +import cheroot.server + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil +from ._cpcompat import tonative + + +class NativeGateway(cheroot.server.Gateway): + """Native gateway implementation allowing to bypass WSGI.""" + + recursive = False + + def respond(self): + """Obtain response from CherryPy machinery and then send it.""" + req = self.req + try: + # Obtain a Request object from CherryPy + local = req.server.bind_addr # FIXME: handle UNIX sockets + local = tonative(local[0]), local[1] + local = httputil.Host(local[0], local[1], '') + remote = tonative(req.conn.remote_addr), req.conn.remote_port + remote = httputil.Host(remote[0], remote[1], '') + + scheme = tonative(req.scheme) + sn = cherrypy.tree.script_name(tonative(req.uri or '/')) + if sn is None: + self.send_response('404 Not Found', [], ['']) + else: + app = cherrypy.tree.apps[sn] + method = tonative(req.method) + path = tonative(req.path) + qs = tonative(req.qs or '') + headers = ( + (tonative(h), tonative(v)) + for h, v in req.inheaders.items() + ) + rfile = req.rfile + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving( + local, remote, scheme, 'HTTP/1.1') + request.multithread = True + request.multiprocess = False + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the + # response + try: + request.run( + method, path, qs, + tonative(req.request_protocol), + headers, rfile, + ) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not self.recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same ' + 'URL twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + self.send_response( + response.output_status, response.header_list, + response.body) + finally: + app.release_serving() + except Exception: + tb = format_exc() + # print tb + cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) + s, h, b = bare_error() + self.send_response(s, h, b) + + def send_response(self, status, headers, body): + """Send response to HTTP request.""" + req = self.req + + # Set response status + req.status = status or b'500 Server Error' + + # Set response headers + for header, value in headers: + req.outheaders.append((header, value)) + if (req.ready and not req.sent_headers): + req.sent_headers = True + req.send_headers() + + # Set response body + for seg in body: + req.write(seg) + + +class CPHTTPServer(cheroot.server.HTTPServer): + """Wrapper for cheroot.server.HTTPServer. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. + Therefore, we wrap it here, so we can apply some attributes + from config -> cherrypy.server -> HTTPServer. + """ + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPHTTPServer.""" + self.server_adapter = server_adapter + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + cheroot.server.HTTPServer.__init__( + self, server_adapter.bind_addr, NativeGateway, + minthreads=server_adapter.thread_pool, + maxthreads=server_adapter.thread_pool_max, + server_name=server_name) + + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0) + self.request_queue_size = self.server_adapter.socket_queue_size + self.timeout = self.server_adapter.socket_timeout + self.shutdown_timeout = self.server_adapter.shutdown_timeout + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpreqbody.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpreqbody.py new file mode 100644 index 000000000..4d3cefe76 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpreqbody.py @@ -0,0 +1,993 @@ +"""Request body processing for CherryPy. + +.. versionadded:: 3.2 + +Application authors have complete control over the parsing of HTTP request +entities. In short, +:attr:`cherrypy.request.body` +is now always set to an instance of +:class:`RequestBody`, +and *that* class is a subclass of :class:`Entity`. + +When an HTTP request includes an entity body, it is often desirable to +provide that information to applications in a form other than the raw bytes. +Different content types demand different approaches. Examples: + + * For a GIF file, we want the raw bytes in a stream. + * An HTML form is better parsed into its component fields, and each text field + decoded from bytes to unicode. + * A JSON body should be deserialized into a Python dict or list. + +When the request contains a Content-Type header, the media type is used as a +key to look up a value in the +:attr:`request.body.processors` dict. +If the full media +type is not found, then the major type is tried; for example, if no processor +is found for the 'image/jpeg' type, then we look for a processor for the +'image' types altogether. If neither the full type nor the major type has a +matching processor, then a default processor is used +(:func:`default_proc`). For most +types, this means no processing is done, and the body is left unread as a +raw byte stream. Processors are configurable in an 'on_start_resource' hook. + +Some processors, especially those for the 'text' types, attempt to decode bytes +to unicode. If the Content-Type request header includes a 'charset' parameter, +this is used to decode the entity. Otherwise, one or more default charsets may +be attempted, although this decision is up to each processor. If a processor +successfully decodes an Entity or Part, it should set the +:attr:`charset` attribute +on the Entity or Part to the name of the successful charset, so that +applications can easily re-encode or transcode the value if they wish. + +If the Content-Type of the request entity is of major type 'multipart', then +the above parsing process, and possibly a decoding process, is performed for +each part. + +For both the full entity and multipart parts, a Content-Disposition header may +be used to fill :attr:`name` and +:attr:`filename` attributes on the +request.body or the Part. + +.. _custombodyprocessors: + +Custom Processors +================= + +You can add your own processors for any specific or major MIME type. Simply add +it to the :attr:`processors` dict in a +hook/tool that runs at ``on_start_resource`` or ``before_request_body``. +Here's the built-in JSON tool for an example:: + + def json_in(force=True, debug=False): + request = cherrypy.serving.request + def json_processor(entity): + '''Read application/json data into request.json.''' + if not entity.headers.get("Content-Length", ""): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + request.json = json_decode(body) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + if force: + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an application/json content type') + request.body.processors['application/json'] = json_processor + +We begin by defining a new ``json_processor`` function to stick in the +``processors`` dictionary. All processor functions take a single argument, +the ``Entity`` instance they are to process. It will be called whenever a +request is received (for those URI's where the tool is turned on) which +has a ``Content-Type`` of "application/json". + +First, it checks for a valid ``Content-Length`` (raising 411 if not valid), +then reads the remaining bytes on the socket. The ``fp`` object knows its +own length, so it won't hang waiting for data that never arrives. It will +return when all data has been read. Then, we decode those bytes using +Python's built-in ``json`` module, and stick the decoded result onto +``request.json`` . If it cannot be decoded, we raise 400. + +If the "force" argument is True (the default), the ``Tool`` clears the +``processors`` dict so that request entities of other ``Content-Types`` +aren't parsed at all. Since there's no entry for those invalid MIME +types, the ``default_proc`` method of ``cherrypy.request.body`` is +called. But this does nothing by default (usually to provide the page +handler an opportunity to handle it.) +But in our case, we want to raise 415, so we replace +``request.body.default_proc`` +with the error (``HTTPError`` instances, when called, raise themselves). + +If we were defining a custom processor, we can do so without making a ``Tool``. +Just add the config entry:: + + request.body.processors = {'application/json': json_processor} + +Note that you can only replace the ``processors`` dict wholesale this way, +not update the existing one. +""" + +try: + from io import DEFAULT_BUFFER_SIZE +except ImportError: + DEFAULT_BUFFER_SIZE = 8192 +import re +import sys +import tempfile +from urllib.parse import unquote + +import cheroot.server + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.lib import httputil + + +def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(b'+', b' ') + atoms = bs.split(b'%') + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return b''.join(atoms) + + +# ------------------------------- Processors -------------------------------- # + +def process_urlencoded(entity): + """Read application/x-www-form-urlencoded data into entity.params.""" + qs = entity.fp.read() + for charset in entity.attempt_charsets: + try: + params = {} + for aparam in qs.split(b'&'): + for pair in aparam.split(b';'): + if not pair: + continue + + atoms = pair.split(b'=', 1) + if len(atoms) == 1: + atoms.append(b'') + + key = unquote_plus(atoms[0]).decode(charset) + value = unquote_plus(atoms[1]).decode(charset) + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + except UnicodeDecodeError: + pass + else: + entity.charset = charset + break + else: + raise cherrypy.HTTPError( + 400, 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(entity.attempt_charsets)) + + # Now that all values have been successfully parsed and decoded, + # apply them to the entity.params dict. + for key, value in params.items(): + if key in entity.params: + if not isinstance(entity.params[key], list): + entity.params[key] = [entity.params[key]] + entity.params[key].append(value) + else: + entity.params[key] = value + + +def process_multipart(entity): + """Read all multipart parts into entity.parts.""" + ib = '' + if 'boundary' in entity.content_type.params: + # http://tools.ietf.org/html/rfc2046#section-5.1.1 + # "The grammar for parameters on the Content-type field is such that it + # is often necessary to enclose the boundary parameter values in quotes + # on the Content-type line" + ib = entity.content_type.params['boundary'].strip('"') + + if not re.match('^[ -~]{0,200}[!-~]$', ib): + raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) + + ib = ('--' + ib).encode('ascii') + + # Find the first marker + while True: + b = entity.readline() + if not b: + return + + b = b.strip() + if b == ib: + break + + # Read all parts + while True: + part = entity.part_class.from_fp(entity.fp, ib) + entity.parts.append(part) + part.process() + if part.fp.done: + break + + +def process_multipart_form_data(entity): + """Read all multipart/form-data parts into entity.parts or entity.params. + """ + process_multipart(entity) + + kept_parts = [] + for part in entity.parts: + if part.name is None: + kept_parts.append(part) + else: + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if part.name in entity.params: + if not isinstance(entity.params[part.name], list): + entity.params[part.name] = [entity.params[part.name]] + entity.params[part.name].append(value) + else: + entity.params[part.name] = value + + entity.parts = kept_parts + + +def _old_process_multipart(entity): + """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" + process_multipart(entity) + + params = entity.params + + for part in entity.parts: + if part.name is None: + key = ntou('parts') + else: + key = part.name + + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + + +# -------------------------------- Entities --------------------------------- # +class Entity(object): + + """An HTTP request body, or MIME multipart body. + + This class collects information about the HTTP request entity. When a + given entity is of MIME type "multipart", each part is parsed into its own + Entity instance, and the set of parts stored in + :attr:`entity.parts`. + + Between the ``before_request_body`` and ``before_handler`` tools, CherryPy + tries to process the request body (if any) by calling + :func:`request.body.process`. + This uses the ``content_type`` of the Entity to look up a suitable + processor in + :attr:`Entity.processors`, + a dict. + If a matching processor cannot be found for the complete Content-Type, + it tries again using the major type. For example, if a request with an + entity of type "image/jpeg" arrives, but no processor can be found for + that complete type, then one is sought for the major type "image". If a + processor is still not found, then the + :func:`default_proc` method + of the Entity is called (which does nothing by default; you can + override this too). + + CherryPy includes processors for the "application/x-www-form-urlencoded" + type, the "multipart/form-data" type, and the "multipart" major type. + CherryPy 3.2 processes these types almost exactly as older versions. + Parts are passed as arguments to the page handler using their + ``Content-Disposition.name`` if given, otherwise in a generic "parts" + argument. Each such part is either a string, or the + :class:`Part` itself if it's a file. (In this + case it will have ``file`` and ``filename`` attributes, or possibly a + ``value`` attribute). Each Part is itself a subclass of + Entity, and has its own ``process`` method and ``processors`` dict. + + There is a separate processor for the "multipart" major type which is more + flexible, and simply stores all multipart parts in + :attr:`request.body.parts`. You can + enable it with:: + + cherrypy.request.body.processors['multipart'] = \ + _cpreqbody.process_multipart + + in an ``on_start_resource`` tool. + """ + + # http://tools.ietf.org/html/rfc2046#section-4.1.2: + # "The default character set, which must be assumed in the + # absence of a charset parameter, is US-ASCII." + # However, many browsers send data in utf-8 with no charset. + attempt_charsets = ['utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + charset = None + """The successful decoding; see "attempt_charsets" above.""" + + content_type = None + """The value of the Content-Type request header. + + If the Entity is part of a multipart payload, this will be the Content-Type + given in the MIME headers for this part. + """ + + default_content_type = 'application/x-www-form-urlencoded' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + filename = None + """The ``Content-Disposition.filename`` header, if available.""" + + fp = None + """The readable socket file object.""" + + headers = None + """A dict of request/multipart header names and values. + + This is a copy of the ``request.headers`` for the ``request.body``; + for multipart parts, it is the set of headers for that part. + """ + + length = None + """The value of the ``Content-Length`` header, if provided.""" + + name = None + """The "name" parameter of the ``Content-Disposition`` header, if any.""" + + params = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True).""" + + processors = {'application/x-www-form-urlencoded': process_urlencoded, + 'multipart/form-data': process_multipart_form_data, + 'multipart': process_multipart, + } + """A dict of Content-Type names to processor methods.""" + + parts = None + """A list of Part instances if ``Content-Type`` is of major type + "multipart".""" + + part_class = None + """The class used for multipart parts. + + You can replace this with custom subclasses to alter the processing of + multipart parts. + """ + + def __init__(self, fp, headers, params=None, parts=None): + # Make an instance-specific copy of the class processors + # so Tools, etc. can replace them per-request. + self.processors = self.processors.copy() + + self.fp = fp + self.headers = headers + + if params is None: + params = {} + self.params = params + + if parts is None: + parts = [] + self.parts = parts + + # Content-Type + self.content_type = headers.elements('Content-Type') + if self.content_type: + self.content_type = self.content_type[0] + else: + self.content_type = httputil.HeaderElement.from_str( + self.default_content_type) + + # Copy the class 'attempt_charsets', prepending any Content-Type + # charset + dec = self.content_type.params.get('charset', None) + if dec: + self.attempt_charsets = [dec] + [c for c in self.attempt_charsets + if c != dec] + else: + self.attempt_charsets = self.attempt_charsets[:] + + # Length + self.length = None + clen = headers.get('Content-Length', None) + # If Transfer-Encoding is 'chunked', ignore any Content-Length. + if ( + clen is not None and + 'chunked' not in headers.get('Transfer-Encoding', '') + ): + try: + self.length = int(clen) + except ValueError: + pass + + # Content-Disposition + self.name = None + self.filename = None + disp = headers.elements('Content-Disposition') + if disp: + disp = disp[0] + if 'name' in disp.params: + self.name = disp.params['name'] + if self.name.startswith('"') and self.name.endswith('"'): + self.name = self.name[1:-1] + if 'filename' in disp.params: + self.filename = disp.params['filename'] + if ( + self.filename.startswith('"') and + self.filename.endswith('"') + ): + self.filename = self.filename[1:-1] + if 'filename*' in disp.params: + # @see https://tools.ietf.org/html/rfc5987 + encoding, lang, filename = disp.params['filename*'].split("'") + self.filename = unquote(str(filename), encoding) + + def read(self, size=None, fp_out=None): + return self.fp.read(size, fp_out) + + def readline(self, size=None): + return self.fp.readline(size) + + def readlines(self, sizehint=None): + return self.fp.readlines(sizehint) + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def next(self): + return self.__next__() + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read(fp_out=fp_out) + return fp_out + + def make_file(self): + """Return a file-like object into which the request body will be read. + + By default, this will return a TemporaryFile. Override as needed. + See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" + return tempfile.TemporaryFile() + + def fullvalue(self): + """Return this entity as a string, whether stored in a file or not.""" + if self.file: + # It was stored in a tempfile. Read it. + self.file.seek(0) + value = self.file.read() + self.file.seek(0) + else: + value = self.value + value = self.decode_entity(value) + return value + + def decode_entity(self, value): + """Return a given byte encoded value as a string""" + for charset in self.attempt_charsets: + try: + value = value.decode(charset) + except UnicodeDecodeError: + pass + else: + self.charset = charset + return value + else: + raise cherrypy.HTTPError( + 400, + 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(self.attempt_charsets) + ) + + def process(self): + """Execute the best-match processor for the given media type.""" + proc = None + ct = self.content_type.value + try: + proc = self.processors[ct] + except KeyError: + toptype = ct.split('/', 1)[0] + try: + proc = self.processors[toptype] + except KeyError: + pass + if proc is None: + self.default_proc() + else: + proc(self) + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + # Leave the fp alone for someone else to read. This works fine + # for request.body, but the Part subclasses need to override this + # so they can move on to the next part. + pass + + +class Part(Entity): + + """A MIME part entity, part of a multipart entity.""" + + # "The default character set, which must be assumed in the absence of a + # charset parameter, is US-ASCII." + attempt_charsets = ['us-ascii', 'utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + boundary = None + """The MIME multipart boundary.""" + + default_content_type = 'text/plain' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however (this class), + the MIME spec declares that a part with no Content-Type defaults to + "text/plain". + """ + + # This is the default in stdlib cgi. We may want to increase it. + maxrambytes = 1000 + """The threshold of bytes after which point the ``Part`` will store + its data in a file (generated by + :func:`make_file`) + instead of a string. Defaults to 1000, just like the :mod:`cgi` + module in Python's standard library. + """ + + def __init__(self, fp, headers, boundary): + Entity.__init__(self, fp, headers) + self.boundary = boundary + self.file = None + self.value = None + + @classmethod + def from_fp(cls, fp, boundary): + headers = cls.read_headers(fp) + return cls(fp, headers, boundary) + + @classmethod + def read_headers(cls, fp): + headers = httputil.HeaderMap() + while True: + line = fp.readline() + if not line: + # No more data--illegal end of headers + raise EOFError('Illegal end of headers.') + + if line == b'\r\n': + # Normal end of headers + break + if not line.endswith(b'\r\n'): + raise ValueError('MIME requires CRLF terminators: %r' % line) + + if line[0] in b' \t': + # It's a continuation line. + v = line.strip().decode('ISO-8859-1') + else: + k, v = line.split(b':', 1) + k = k.strip().decode('ISO-8859-1') + v = v.strip().decode('ISO-8859-1') + + existing = headers.get(k) + if existing: + v = ', '.join((existing, v)) + headers[k] = v + + return headers + + def read_lines_to_boundary(self, fp_out=None): + """Read bytes from self.fp and return or write them to a file. + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and that fp is returned. + """ + endmarker = self.boundary + b'--' + delim = b'' + prev_lf = True + lines = [] + seen = 0 + while True: + line = self.fp.readline(1 << 16) + if not line: + raise EOFError('Illegal end of multipart body.') + if line.startswith(b'--') and prev_lf: + strippedline = line.strip() + if strippedline == self.boundary: + break + if strippedline == endmarker: + self.fp.finish() + break + + line = delim + line + + if line.endswith(b'\r\n'): + delim = b'\r\n' + line = line[:-2] + prev_lf = True + elif line.endswith(b'\n'): + delim = b'\n' + line = line[:-1] + prev_lf = True + else: + delim = b'' + prev_lf = False + + if fp_out is None: + lines.append(line) + seen += len(line) + if seen > self.maxrambytes: + fp_out = self.make_file() + for line in lines: + fp_out.write(line) + else: + fp_out.write(line) + + if fp_out is None: + result = b''.join(lines) + return result + else: + fp_out.seek(0) + return fp_out + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + if self.filename: + # Always read into a file if a .filename was given. + self.file = self.read_into_file() + else: + result = self.read_lines_to_boundary() + if isinstance(result, bytes): + self.value = result + else: + self.file = result + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read_lines_to_boundary(fp_out=fp_out) + return fp_out + + +Entity.part_class = Part + +inf = float('inf') + + +class SizedReader: + + def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, + has_trailers=False): + # Wrap our fp in a buffer so peek() works + self.fp = fp + self.length = length + self.maxbytes = maxbytes + self.buffer = b'' + self.bufsize = bufsize + self.bytes_read = 0 + self.done = False + self.has_trailers = has_trailers + + def read(self, size=None, fp_out=None): + """Read bytes from the request body and return or write them to a file. + + A number of bytes less than or equal to the 'size' argument are read + off the socket. The actual number of bytes read are tracked in + self.bytes_read. The number may be smaller than 'size' when 1) the + client sends fewer bytes, 2) the 'Content-Length' request header + specifies fewer bytes than requested, or 3) the number of bytes read + exceeds self.maxbytes (in which case, 413 is raised). + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and None is returned. + """ + + if self.length is None: + if size is None: + remaining = inf + else: + remaining = size + else: + remaining = self.length - self.bytes_read + if size and size < remaining: + remaining = size + if remaining == 0: + self.finish() + if fp_out is None: + return b'' + else: + return None + + chunks = [] + + # Read bytes from the buffer. + if self.buffer: + if remaining is inf: + data = self.buffer + self.buffer = b'' + else: + data = self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + # Read bytes from the socket. + while remaining > 0: + chunksize = min(remaining, self.bufsize) + try: + data = self.fp.read(chunksize) + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + if not data: + self.finish() + break + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + if fp_out is None: + return b''.join(chunks) + + def readline(self, size=None): + """Read a line from the request body and return it.""" + chunks = [] + while size is None or size > 0: + chunksize = self.bufsize + if size is not None and size < self.bufsize: + chunksize = size + data = self.read(chunksize) + if not data: + break + pos = data.find(b'\n') + 1 + if pos: + chunks.append(data[:pos]) + remainder = data[pos:] + self.buffer += remainder + self.bytes_read -= len(remainder) + break + else: + chunks.append(data) + return b''.join(chunks) + + def readlines(self, sizehint=None): + """Read lines from the request body and return them.""" + if self.length is not None: + if sizehint is None: + sizehint = self.length - self.bytes_read + else: + sizehint = min(sizehint, self.length - self.bytes_read) + + lines = [] + seen = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + seen += len(line) + if seen >= sizehint: + break + return lines + + def finish(self): + self.done = True + if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): + self.trailers = {} + + try: + for line in self.fp.read_trailer_lines(): + if line[0] in b' \t': + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(b':', 1) + except ValueError: + raise ValueError('Illegal header line.') + k = k.strip().title() + v = v.strip() + + if k in cheroot.server.comma_separated_headers: + existing = self.trailers.get(k) + if existing: + v = b', '.join((existing, v)) + self.trailers[k] = v + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + + +class RequestBody(Entity): + + """The entity of the HTTP request.""" + + bufsize = 8 * 1024 + """The buffer size used when reading the socket.""" + + # Don't parse the request body at all if the client didn't provide + # a Content-Type header. See + # https://github.com/cherrypy/cherrypy/issues/790 + default_content_type = '' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + maxbytes = None + """Raise ``MaxSizeExceeded`` if more bytes than this are read from + the socket. + """ + + def __init__(self, fp, headers, params=None, request_params=None): + Entity.__init__(self, fp, headers, params) + + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 + # When no explicit charset parameter is provided by the + # sender, media subtypes of the "text" type are defined + # to have a default charset value of "ISO-8859-1" when + # received via HTTP. + if self.content_type.value.startswith('text/'): + for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): + if c in self.attempt_charsets: + break + else: + self.attempt_charsets.append('ISO-8859-1') + + # Temporary fix while deprecating passing .parts as .params. + self.processors['multipart'] = _old_process_multipart + + if request_params is None: + request_params = {} + self.request_params = request_params + + def process(self): + """Process the request entity based on its Content-Type.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # It is possible to send a POST request with no body, for example; + # however, app developers are responsible in that case to set + # cherrypy.request.process_body to False so this method isn't called. + h = cherrypy.serving.request.headers + if 'Content-Length' not in h and 'Transfer-Encoding' not in h: + raise cherrypy.HTTPError(411) + + self.fp = SizedReader(self.fp, self.length, + self.maxbytes, bufsize=self.bufsize, + has_trailers='Trailer' in h) + super(RequestBody, self).process() + + # Body params should also be a part of the request_params + # add them in here. + request_params = self.request_params + for key, value in self.params.items(): + if key in request_params: + if not isinstance(request_params[key], list): + request_params[key] = [request_params[key]] + request_params[key].append(value) + else: + request_params[key] = value diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cprequest.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cprequest.py new file mode 100644 index 000000000..a661112c6 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cprequest.py @@ -0,0 +1,935 @@ +import sys +import time +import collections +import operator +from http.cookies import SimpleCookie, CookieError + +import uuid + +from more_itertools import consume + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy import _cpreqbody +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil, reprconf, encoding + + +class Hook(object): + + """A callback and its metadata: failsafe, priority, and kwargs.""" + + callback = None + """ + The bare callable that this Hook object is wrapping, which will + be called when the Hook is called.""" + + failsafe = False + """ + If True, the callback is guaranteed to run even if other callbacks + from the same call point raise exceptions.""" + + priority = 50 + """ + Defines the order of execution for a list of Hooks. Priority numbers + should be limited to the closed interval [0, 100], but values outside + this range are acceptable, as are fractional values.""" + + kwargs = {} + """ + A set of keyword arguments that will be passed to the + callable on each call.""" + + def __init__(self, callback, failsafe=None, priority=None, **kwargs): + self.callback = callback + + if failsafe is None: + failsafe = getattr(callback, 'failsafe', False) + self.failsafe = failsafe + + if priority is None: + priority = getattr(callback, 'priority', 50) + self.priority = priority + + self.kwargs = kwargs + + def __lt__(self, other): + """ + Hooks sort by priority, ascending, such that + hooks of lower priority are run first. + """ + return self.priority < other.priority + + def __call__(self): + """Run self.callback(**self.kwargs).""" + return self.callback(**self.kwargs) + + def __repr__(self): + cls = self.__class__ + return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)' + % (cls.__module__, cls.__name__, self.callback, + self.failsafe, self.priority, + ', '.join(['%s=%r' % (k, v) + for k, v in self.kwargs.items()]))) + + +class HookMap(dict): + + """A map of call points to lists of callbacks (Hook objects).""" + + def __new__(cls, points=None): + d = dict.__new__(cls) + for p in points or []: + d[p] = [] + return d + + def __init__(self, *a, **kw): + pass + + def attach(self, point, callback, failsafe=None, priority=None, **kwargs): + """Append a new Hook made from the supplied arguments.""" + self[point].append(Hook(callback, failsafe, priority, **kwargs)) + + def run(self, point): + """Execute all registered Hooks (callbacks) for the given point.""" + self.run_hooks(iter(sorted(self[point]))) + + @classmethod + def run_hooks(cls, hooks): + """Execute the indicated hooks, trapping errors. + + Hooks with ``.failsafe == True`` are guaranteed to run + even if others at the same hookpoint fail. In this case, + log the failure and proceed on to the next hook. The only + way to stop all processing from one of these hooks is + to raise a BaseException like SystemExit or + KeyboardInterrupt and stop the whole server. + """ + assert isinstance(hooks, collections.abc.Iterator) + quiet_errors = ( + cherrypy.HTTPError, + cherrypy.HTTPRedirect, + cherrypy.InternalRedirect, + ) + safe = filter(operator.attrgetter('failsafe'), hooks) + for hook in hooks: + try: + hook() + except quiet_errors: + cls.run_hooks(safe) + raise + except Exception: + cherrypy.log(traceback=True, severity=40) + cls.run_hooks(safe) + raise + + def __copy__(self): + newmap = self.__class__() + # We can't just use 'update' because we want copies of the + # mutable values (each is a list) as well. + for k, v in self.items(): + newmap[k] = v[:] + return newmap + copy = __copy__ + + def __repr__(self): + cls = self.__class__ + return '%s.%s(points=%r)' % ( + cls.__module__, + cls.__name__, + list(self) + ) + + +# Config namespace handlers + +def hooks_namespace(k, v): + """Attach bare hooks declared in config.""" + # Use split again to allow multiple hooks for a single + # hookpoint per path (e.g. "hooks.before_handler.1"). + # Little-known fact you only get from reading source ;) + hookpoint = k.split('.', 1)[0] + if isinstance(v, str): + v = cherrypy.lib.reprconf.attributes(v) + if not isinstance(v, Hook): + v = Hook(v) + cherrypy.serving.request.hooks[hookpoint].append(v) + + +def request_namespace(k, v): + """Attach request attributes declared in config.""" + # Provides config entries to set request.body attrs (like + # attempt_charsets). + if k[:5] == 'body.': + setattr(cherrypy.serving.request.body, k[5:], v) + else: + setattr(cherrypy.serving.request, k, v) + + +def response_namespace(k, v): + """Attach response attributes declared in config.""" + # Provides config entries to set default response headers + # http://cherrypy.dev/ticket/889 + if k[:8] == 'headers.': + cherrypy.serving.response.headers[k.split('.', 1)[1]] = v + else: + setattr(cherrypy.serving.response, k, v) + + +def error_page_namespace(k, v): + """Attach error pages declared in config.""" + if k != 'default': + k = int(k) + cherrypy.serving.request.error_page[k] = v + + +hookpoints = ['on_start_resource', 'before_request_body', + 'before_handler', 'before_finalize', + 'on_end_resource', 'on_end_request', + 'before_error_response', 'after_error_response'] + + +class Request(object): + + """An HTTP request. + + This object represents the metadata of an HTTP request message; + that is, it contains attributes which describe the environment + in which the request URL, headers, and body were sent (if you + want tools to interpret the headers and body, those are elsewhere, + mostly in Tools). This 'metadata' consists of socket data, + transport characteristics, and the Request-Line. This object + also contains data regarding the configuration in effect for + the given URL, and the execution plan for generating a response. + """ + + prev = None + """ + The previous Request object (if any). This should be None + unless we are processing an InternalRedirect.""" + + # Conversation/connection attributes + local = httputil.Host('127.0.0.1', 80) + 'An httputil.Host(ip, port, hostname) object for the server socket.' + + remote = httputil.Host('127.0.0.1', 1111) + 'An httputil.Host(ip, port, hostname) object for the client socket.' + + scheme = 'http' + """ + The protocol used between client and server. In most cases, + this will be either 'http' or 'https'.""" + + server_protocol = 'HTTP/1.1' + """ + The HTTP version for which the HTTP server is at least + conditionally compliant.""" + + base = '' + """The (scheme://host) portion of the requested URL. + In some cases (e.g. when proxying via mod_rewrite), this may contain + path segments which cherrypy.url uses when constructing url's, but + which otherwise are ignored by CherryPy. Regardless, this value + MUST NOT end in a slash.""" + + # Request-Line attributes + request_line = '' + """ + The complete Request-Line received from the client. This is a + single string consisting of the request method, URI, and protocol + version (joined by spaces). Any final CRLF is removed.""" + + method = 'GET' + """ + Indicates the HTTP method to be performed on the resource identified + by the Request-URI. Common methods include GET, HEAD, POST, PUT, and + DELETE. CherryPy allows any extension method; however, various HTTP + servers and gateways may restrict the set of allowable methods. + CherryPy applications SHOULD restrict the set (on a per-URI basis).""" + + query_string = '' + """ + The query component of the Request-URI, a string of information to be + interpreted by the resource. The query portion of a URI follows the + path component, and is separated by a '?'. For example, the URI + 'http://www.cherrypy.dev/wiki?a=3&b=4' has the query component, + 'a=3&b=4'.""" + + query_string_encoding = 'utf8' + """ + The encoding expected for query string arguments after % HEX HEX decoding). + If a query string is provided that cannot be decoded with this encoding, + 404 is raised (since technically it's a different URI). If you want + arbitrary encodings to not error, set this to 'Latin-1'; you can then + encode back to bytes and re-decode to whatever encoding you like later. + """ + + protocol = (1, 1) + """The HTTP protocol version corresponding to the set + of features which should be allowed in the response. If BOTH + the client's request message AND the server's level of HTTP + compliance is HTTP/1.1, this attribute will be the tuple (1, 1). + If either is 1.0, this attribute will be the tuple (1, 0). + Lower HTTP protocol versions are not explicitly supported.""" + + params = {} + """ + A dict which combines query string (GET) and request entity (POST) + variables. This is populated in two stages: GET params are added + before the 'on_start_resource' hook, and POST params are added + between the 'before_request_body' and 'before_handler' hooks.""" + + # Message attributes + header_list = [] + """ + A list of the HTTP request headers as (name, value) tuples. + In general, you should use request.headers (a dict) instead.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the request headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). See also: + httputil.HeaderMap, httputil.HeaderElement.""" + + cookie = SimpleCookie() + """See help(Cookie).""" + + rfile = None + """ + If the request included an entity (body), it will be available + as a stream in this attribute. However, the rfile will normally + be read for you between the 'before_request_body' hook and the + 'before_handler' hook, and the resulting string is placed into + either request.params or the request.body attribute. + + You may disable the automatic consumption of the rfile by setting + request.process_request_body to False, either in config for the desired + path, or in an 'on_start_resource' or 'before_request_body' hook. + + WARNING: In almost every case, you should not attempt to read from the + rfile stream after CherryPy's automatic mechanism has read it. If you + turn off the automatic parsing of rfile, you should read exactly the + number of bytes specified in request.headers['Content-Length']. + Ignoring either of these warnings may result in a hung request thread + or in corruption of the next (pipelined) request. + """ + + process_request_body = True + """ + If True, the rfile (if any) is automatically read and parsed, + and the result placed into request.params or request.body.""" + + methods_with_bodies = ('POST', 'PUT', 'PATCH') + """ + A sequence of HTTP methods for which CherryPy will automatically + attempt to read a body from the rfile. If you are going to change + this property, modify it on the configuration (recommended) + or on the "hook point" `on_start_resource`. + """ + + body = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' + or multipart, this will be None. Otherwise, this will be an instance + of :class:`RequestBody` (which you + can .read()); this value is set between the 'before_request_body' and + 'before_handler' hooks (assuming that process_request_body is True).""" + + # Dispatch attributes + dispatch = cherrypy.dispatch.Dispatcher() + """ + The object which looks up the 'page handler' callable and collects + config for the current request based on the path_info, other + request attributes, and the application architecture. The core + calls the dispatcher as early as possible, passing it a 'path_info' + argument. + + The default dispatcher discovers the page handler by matching path_info + to a hierarchical arrangement of objects, starting at request.app.root. + See help(cherrypy.dispatch) for more information.""" + + script_name = '' + """ + The 'mount point' of the application which is handling this request. + + This attribute MUST NOT end in a slash. If the script_name refers to + the root of the URI, it MUST be an empty string (not "/"). + """ + + path_info = '/' + """ + The 'relative path' portion of the Request-URI. This is relative + to the script_name ('mount point') of the application which is + handling this request.""" + + login = None + """ + When authentication is used during the request processing this is + set to 'False' if it failed and to the 'username' value if it succeeded. + The default 'None' implies that no authentication happened.""" + + # Note that cherrypy.url uses "if request.app:" to determine whether + # the call is during a real HTTP request or not. So leave this None. + app = None + """The cherrypy.Application object which is handling this request.""" + + handler = None + """ + The function, method, or other callable which CherryPy will call to + produce the response. The discovery of the handler and the arguments + it will receive are determined by the request.dispatch object. + By default, the handler is discovered by walking a tree of objects + starting at request.app.root, and is then passed all HTTP params + (from the query string and POST body) as keyword arguments.""" + + toolmaps = {} + """ + A nested dict of all Toolboxes and Tools in effect for this request, + of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" + + config = None + """ + A flat dict of all configuration entries which apply to the + current request. These entries are collected from global config, + application config (based on request.path_info), and from handler + config (exactly how is governed by the request.dispatch object in + effect for this request; by default, handler config can be attached + anywhere in the tree between request.app.root and the final handler, + and inherits downward).""" + + is_index = None + """ + This will be True if the current request is mapped to an 'index' + resource handler (also, a 'default' handler if path_info ends with + a slash). The value may be used to automatically redirect the + user-agent to a 'more canonical' URL which either adds or removes + the trailing slash. See cherrypy.tools.trailing_slash.""" + + hooks = HookMap(hookpoints) + """ + A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. + Each key is a str naming the hook point, and each value is a list + of hooks which will be called at that hook point during this request. + The list of hooks is generally populated as early as possible (mostly + from Tools specified in config), but may be extended at any time. + See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" + + error_response = cherrypy.HTTPError(500).set_response + """ + The no-arg callable which will handle unexpected, untrapped errors + during request processing. This is not used for expected exceptions + (like NotFound, HTTPError, or HTTPRedirect) which are raised in + response to expected conditions (those should be customized either + via request.error_page or by overriding HTTPError.set_response). + By default, error_response uses HTTPError(500) to return a generic + error response to the user-agent.""" + + error_page = {} + """ + A dict of {error code: response filename or callable} pairs. + + The error code must be an int representing a given HTTP error code, + or the string 'default', which will be used if no matching entry + is found for a given numeric code. + + If a filename is provided, the file should contain a Python string- + formatting template, and can expect by default to receive format + values with the mapping keys %(status)s, %(message)s, %(traceback)s, + and %(version)s. The set of format mappings can be extended by + overriding HTTPError.set_response. + + If a callable is provided, it will be called by default with keyword + arguments 'status', 'message', 'traceback', and 'version', as for a + string-formatting template. The callable must return a string or + iterable of strings which will be set to response.body. It may also + override headers or perform any other processing. + + If no entry is given for an error code, and no 'default' entry exists, + a default template will be used. + """ + + show_tracebacks = True + """ + If True, unexpected errors encountered during request processing will + include a traceback in the response body.""" + + show_mismatched_params = True + """ + If True, mismatched parameters encountered during PageHandler invocation + processing will be included in the response body.""" + + throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) + """The sequence of exceptions which Request.run does not trap.""" + + throw_errors = False + """ + If True, Request.run will not trap any errors (except HTTPRedirect and + HTTPError, which are more properly called 'exceptions', not errors).""" + + closed = False + """True once the close method has been called, False otherwise.""" + + stage = None + """ + A string containing the stage reached in the request-handling process. + This is useful when debugging a live server with hung requests.""" + + unique_id = None + """A lazy object generating and memorizing UUID4 on ``str()`` render.""" + + namespaces = reprconf.NamespaceSet( + **{'hooks': hooks_namespace, + 'request': request_namespace, + 'response': response_namespace, + 'error_page': error_page_namespace, + 'tools': cherrypy.tools, + }) + + def __init__(self, local_host, remote_host, scheme='http', + server_protocol='HTTP/1.1'): + """Populate a new Request object. + + local_host should be an httputil.Host object with the server info. + remote_host should be an httputil.Host object with the client info. + scheme should be a string, either "http" or "https". + """ + self.local = local_host + self.remote = remote_host + self.scheme = scheme + self.server_protocol = server_protocol + + self.closed = False + + # Put a *copy* of the class error_page into self. + self.error_page = self.error_page.copy() + + # Put a *copy* of the class namespaces into self. + self.namespaces = self.namespaces.copy() + + self.stage = None + + self.unique_id = LazyUUID4() + + def close(self): + """Run cleanup code. (Core)""" + if not self.closed: + self.closed = True + self.stage = 'on_end_request' + self.hooks.run('on_end_request') + self.stage = 'close' + + def run(self, method, path, query_string, req_protocol, headers, rfile): + r"""Process the Request. (Core) + + method, path, query_string, and req_protocol should be pulled directly + from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). + + path + This should be %XX-unquoted, but query_string should not be. + + When using Python 2, they both MUST be byte strings, + not unicode strings. + + When using Python 3, they both MUST be unicode strings, + not byte strings, and preferably not bytes \x00-\xFF + disguised as unicode. + + headers + A list of (name, value) tuples. + + rfile + A file-like object containing the HTTP request entity. + + When run() is done, the returned object should have 3 attributes: + + * status, e.g. "200 OK" + * header_list, a list of (name, value) tuples + * body, an iterable yielding strings + + Consumer code (HTTP servers) should then access these response + attributes to build the outbound stream. + + """ + response = cherrypy.serving.response + self.stage = 'run' + try: + self.error_response = cherrypy.HTTPError(500).set_response + + self.method = method + path = path or '/' + self.query_string = query_string or '' + self.params = {} + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(self.server_protocol[5]), int(self.server_protocol[7]) + self.protocol = min(rp, sp) + response.headers.protocol = self.protocol + + # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). + url = path + if query_string: + url += '?' + query_string + self.request_line = '%s %s %s' % (method, url, req_protocol) + + self.header_list = list(headers) + self.headers = httputil.HeaderMap() + + self.rfile = rfile + self.body = None + + self.cookie = SimpleCookie() + self.handler = None + + # path_info should be the path from the + # app root (script_name) to the handler. + self.script_name = self.app.script_name + self.path_info = pi = path[len(self.script_name):] + + self.stage = 'respond' + self.respond(pi) + + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + else: + # Failure in setup, error handler or finalize. Bypass them. + # Can't use handle_error because we may not have hooks yet. + cherrypy.log(traceback=True, severity=40) + if self.show_tracebacks: + body = format_exc() + else: + body = '' + r = bare_error(body) + response.output_status, response.header_list, response.body = r + + if self.method == 'HEAD': + # HEAD requests MUST NOT return a message-body in the response. + response.body = [] + + try: + cherrypy.log.access() + except Exception: + cherrypy.log.error(traceback=True) + + return response + + def respond(self, path_info): + """Generate a response for the resource at self.path_info. (Core)""" + try: + try: + try: + self._do_respond(path_info) + except (cherrypy.HTTPRedirect, cherrypy.HTTPError): + inst = sys.exc_info()[1] + inst.set_response() + self.stage = 'before_finalize (HTTPError)' + self.hooks.run('before_finalize') + cherrypy.serving.response.finalize() + finally: + self.stage = 'on_end_resource' + self.hooks.run('on_end_resource') + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + self.handle_error() + + def _do_respond(self, path_info): + response = cherrypy.serving.response + + if self.app is None: + raise cherrypy.NotFound() + + self.hooks = self.__class__.hooks.copy() + self.toolmaps = {} + + # Get the 'Host' header, so we can HTTPRedirect properly. + self.stage = 'process_headers' + self.process_headers() + + self.stage = 'get_resource' + self.get_resource(path_info) + + self.body = _cpreqbody.RequestBody( + self.rfile, self.headers, request_params=self.params) + + self.namespaces(self.config) + + self.stage = 'on_start_resource' + self.hooks.run('on_start_resource') + + # Parse the querystring + self.stage = 'process_query_string' + self.process_query_string() + + # Process the body + if self.process_request_body: + if self.method not in self.methods_with_bodies: + self.process_request_body = False + self.stage = 'before_request_body' + self.hooks.run('before_request_body') + if self.process_request_body: + self.body.process() + + # Run the handler + self.stage = 'before_handler' + self.hooks.run('before_handler') + if self.handler: + self.stage = 'handler' + response.body = self.handler() + + # Finalize + self.stage = 'before_finalize' + self.hooks.run('before_finalize') + response.finalize() + + def process_query_string(self): + """Parse the query string into Python structures. (Core)""" + try: + p = httputil.parse_query_string( + self.query_string, encoding=self.query_string_encoding) + except UnicodeDecodeError: + raise cherrypy.HTTPError( + 404, 'The given query string could not be processed. Query ' + 'strings for this resource must be encoded with %r.' % + self.query_string_encoding) + + self.params.update(p) + + def process_headers(self): + """Parse HTTP header data into Python structures. (Core)""" + # Process the headers into self.headers + headers = self.headers + for name, value in self.header_list: + # Call title() now (and use dict.__method__(headers)) + # so title doesn't have to be called twice. + name = name.title() + value = value.strip() + + headers[name] = httputil.decode_TEXT_maybe(value) + + # Some clients, notably Konquoror, supply multiple + # cookies on different lines with the same key. To + # handle this case, store all cookies in self.cookie. + if name == 'Cookie': + try: + self.cookie.load(value) + except CookieError as exc: + raise cherrypy.HTTPError(400, str(exc)) + + if not dict.__contains__(headers, 'Host'): + # All Internet-based HTTP/1.1 servers MUST respond with a 400 + # (Bad Request) status code to any HTTP/1.1 request message + # which lacks a Host header field. + if self.protocol >= (1, 1): + msg = "HTTP/1.1 requires a 'Host' request header." + raise cherrypy.HTTPError(400, msg) + else: + headers['Host'] = httputil.SanitizedHost(dict.get(headers, 'Host')) + + host = dict.get(headers, 'Host') + if not host: + host = self.local.name or self.local.ip + self.base = '%s://%s' % (self.scheme, host) + + def get_resource(self, path): + """Call a dispatcher (which sets self.handler and .config). (Core)""" + # First, see if there is a custom dispatch at this URI. Custom + # dispatchers can only be specified in app.config, not in _cp_config + # (since custom dispatchers may not even have an app.root). + dispatch = self.app.find_config( + path, 'request.dispatch', self.dispatch) + + # dispatch() should set self.handler and self.config + dispatch(path) + + def handle_error(self): + """Handle the last unanticipated exception. (Core)""" + try: + self.hooks.run('before_error_response') + if self.error_response: + self.error_response() + self.hooks.run('after_error_response') + cherrypy.serving.response.finalize() + except cherrypy.HTTPRedirect: + inst = sys.exc_info()[1] + inst.set_response() + cherrypy.serving.response.finalize() + + +class ResponseBody(object): + + """The body of the HTTP response (the response entity).""" + + unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' + 'if you wish to return unicode.') + + def __get__(self, obj, objclass=None): + if obj is None: + # When calling on the class instead of an instance... + return self + else: + return obj._body + + def __set__(self, obj, value): + # Convert the given value to an iterable object. + if isinstance(value, str): + raise ValueError(self.unicode_err) + elif isinstance(value, list): + # every item in a list must be bytes... + if any(isinstance(item, str) for item in value): + raise ValueError(self.unicode_err) + + obj._body = encoding.prepare_iter(value) + + +class Response(object): + + """An HTTP Response, including status, headers, and body.""" + + status = '' + """The HTTP Status-Code and Reason-Phrase.""" + + header_list = [] + """ + A list of the HTTP response headers as (name, value) tuples. + In general, you should use response.headers (a dict) instead. This + attribute is generated from response.headers and is not valid until + after the finalize phase.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the response headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). + + .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` + """ + + cookie = SimpleCookie() + """See help(Cookie).""" + + body = ResponseBody() + """The body (entity) of the HTTP response.""" + + time = None + """The value of time.time() when created. Use in HTTP dates.""" + + stream = False + """If False, buffer the response body.""" + + def __init__(self): + self.status = None + self.header_list = None + self._body = [] + self.time = time.time() + + self.headers = httputil.HeaderMap() + # Since we know all our keys are titled strings, we can + # bypass HeaderMap.update and get a big speed boost. + dict.update(self.headers, { + 'Content-Type': 'text/html', + 'Server': 'CherryPy/' + cherrypy.__version__, + 'Date': httputil.HTTPDate(self.time), + }) + self.cookie = SimpleCookie() + + def collapse_body(self): + """Collapse self.body to a single string; replace it and return it.""" + new_body = b''.join(self.body) + self.body = new_body + return new_body + + def _flush_body(self): + """ + Discard self.body but consume any generator such that + any finalization can occur, such as is required by + caching.tee_output(). + """ + consume(iter(self.body)) + + def finalize(self): + """Transform headers (and cookies) into self.header_list. (Core)""" + try: + code, reason, _ = httputil.valid_status(self.status) + except ValueError: + raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) + + headers = self.headers + + self.status = '%s %s' % (code, reason) + self.output_status = ntob(str(code), 'ascii') + \ + b' ' + headers.encode(reason) + + if self.stream: + # The upshot: wsgiserver will chunk the response if + # you pop Content-Length (or set it explicitly to None). + # Note that lib.static sets C-L to the file's st_size. + if dict.get(headers, 'Content-Length') is None: + dict.pop(headers, 'Content-Length', None) + elif code < 200 or code in (204, 205, 304): + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." + dict.pop(headers, 'Content-Length', None) + self._flush_body() + self.body = b'' + else: + # Responses which are not streamed should have a Content-Length, + # but allow user code to set Content-Length if desired. + if dict.get(headers, 'Content-Length') is None: + content = self.collapse_body() + dict.__setitem__(headers, 'Content-Length', len(content)) + + # Transform our header dict into a list of tuples. + self.header_list = h = headers.output() + + cookie = self.cookie.output() + if cookie: + for line in cookie.split('\r\n'): + name, value = line.split(': ', 1) + if isinstance(name, str): + name = name.encode('ISO-8859-1') + if isinstance(value, str): + value = headers.encode(value) + h.append((name, value)) + + +class LazyUUID4(object): + def __str__(self): + """Return UUID4 and keep it for future calls.""" + return str(self.uuid4) + + @property + def uuid4(self): + """Provide unique id on per-request basis using UUID4. + + It's evaluated lazily on render. + """ + try: + self._uuid4 + except AttributeError: + # evaluate on first access + self._uuid4 = uuid.uuid4() + + return self._uuid4 diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpserver.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpserver.py new file mode 100644 index 000000000..5f8d98fa3 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpserver.py @@ -0,0 +1,241 @@ +"""Manage HTTP servers with CherryPy.""" + +import cherrypy +from cherrypy.lib.reprconf import attributes +from cherrypy._cpcompat import text_or_bytes +from cherrypy.process.servers import ServerAdapter + + +__all__ = ('Server', ) + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + + @property + def socket_host(self): # noqa: D401; irrelevant for properties + """The hostname or IP address on which to listen for connections. + + Host values may be any IPv4 or IPv6 address, or any valid hostname. + The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if + your hosts file prefers IPv6). The string '0.0.0.0' is a special + IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' + is the similar IN6ADDR_ANY for IPv6. The empty string or None are + not allowed. + """ + return self._socket_host + + @socket_host.setter + def socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + 'interfaces (INADDR_ANY).') + self._socket_host = value + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + accepted_queue_size = -1 + """The maximum number of requests which will be queued up before + the server refuses to accept it (default -1, meaning no limit).""" + + accepted_queue_timeout = 10 + """The timeout in seconds for attempting to add a request to the + queue when the queue is full (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit. + """ + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. + If exceeded, the HTTP server should return "413 Request Entity Too Large". + """ + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + cheroot.wsgi.Server) which cherrypy.server will control. + Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + ssl_ciphers = None + """The ciphers list of SSL.""" + + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with + the builtin WSGI server. Builtin options are: 'builtin' (to + use the SSL library built into recent versions of Python). + You may also register your own classes in the + cheroot.server.ssl_adapters dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. + """ + + peercreds = False + """If True, peer cred lookup for UNIX domain socket will put to WSGI env. + + This information will then be available through WSGI env vars: + * X_REMOTE_PID + * X_REMOTE_UID + * X_REMOTE_GID + """ + + peercreds_resolve = False + """If True, username/group will be looked up in the OS from peercreds. + + This information will then be available through WSGI env vars: + * REMOTE_USER + * X_REMOTE_USER + * X_REMOTE_GROUP + """ + + def __init__(self): + """Initialize Server instance.""" + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, text_or_bytes): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + super(Server, self).start() + start.priority = 75 + + @property + def bind_addr(self): + """Return bind address. + + A (host, port) tuple for TCP sockets or a str for Unix domain sockts. + """ + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + + @bind_addr.setter + def bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, text_or_bytes): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError('bind_addr must be a (host, port) tuple ' + '(for TCP sockets) or a string (for Unix ' + 'domain sockets), not %r' % value) + + def base(self): + """Return the base for this server. + + e.i. scheme://host[:port] or sock file + """ + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptools.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptools.py new file mode 100644 index 000000000..716f99a49 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptools.py @@ -0,0 +1,502 @@ +"""CherryPy tools. A "tool" is any helper, adapted to CP. + +Tools are usually designed to be used in a variety of ways (although some +may only offer one if they choose): + + Library calls + All tools are callables that can be used wherever needed. + The arguments are straightforward and should be detailed within the + docstring. + + Function decorators + All tools, when called, may be used as decorators which configure + individual CherryPy page handlers (methods on the CherryPy tree). + That is, "@tools.anytool()" should "turn on" the tool via the + decorated function's _cp_config attribute. + + CherryPy config + If a tool exposes a "_setup" callable, it will be called + once per Request (if the feature is "turned on" via config). + +Tools may be implemented as any object with a namespace. The builtins +are generally either modules or instances of the tools.Tool class. +""" + +import cherrypy +from cherrypy._helper import expose + +from cherrypy.lib import cptools, encoding, static, jsontools +from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc +from cherrypy.lib import caching as _caching +from cherrypy.lib import auth_basic, auth_digest + + +def _getargs(func): + """Return the names of all static arguments to the given function.""" + # Use this instead of importing inspect for less mem overhead. + import types + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ + return co.co_varnames[:co.co_argcount] + + +_attr_error = ( + 'CherryPy Tools cannot be turned on directly. Instead, turn them ' + 'on via config, or use them as decorators on your page handlers.' +) + + +class Tool(object): + + """A registered function for use with CherryPy request-processing hooks. + + help(tool.callable) should give you more information about this Tool. + """ + + namespace = 'tools' + + def __init__(self, point, callable, name=None, priority=50): + self._point = point + self.callable = callable + self._name = name + self._priority = priority + self.__doc__ = self.callable.__doc__ + self._setargs() + + @property + def on(self): + raise AttributeError(_attr_error) + + @on.setter + def on(self, value): + raise AttributeError(_attr_error) + + def _setargs(self): + """Copy func parameter names to obj attributes.""" + try: + for arg in _getargs(self.callable): + setattr(self, arg, None) + except (TypeError, AttributeError): + if hasattr(self.callable, '__call__'): + for arg in _getargs(self.callable.__call__): + setattr(self, arg, None) + # IronPython 1.0 raises NotImplementedError because + # inspect.getargspec tries to access Python bytecode + # in co_code attribute. + except NotImplementedError: + pass + # IronPython 1B1 may raise IndexError in some cases, + # but if we trap it here it doesn't prevent CP from working. + except IndexError: + pass + + def _merged_args(self, d=None): + """Return a dict of configuration entries for this Tool.""" + if d: + conf = d.copy() + else: + conf = {} + + tm = cherrypy.serving.request.toolmaps[self.namespace] + if self._name in tm: + conf.update(tm[self._name]) + + if 'on' in conf: + del conf['on'] + + return conf + + def __call__(self, *args, **kwargs): + """Compile-time decorator (turn on the tool in config). + + For example:: + + @expose + @tools.proxy() + def whats_my_base(self): + return cherrypy.request.base + """ + if args: + raise TypeError('The %r Tool does not accept positional ' + 'arguments; you must use keyword arguments.' + % self._name) + + def tool_decorator(f): + if not hasattr(f, '_cp_config'): + f._cp_config = {} + subspace = self.namespace + '.' + self._name + '.' + f._cp_config[subspace + 'on'] = True + for k, v in kwargs.items(): + f._cp_config[subspace + k] = v + return f + return tool_decorator + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self.callable, + priority=p, **conf) + + +class HandlerTool(Tool): + + """Tool which is called 'before main', that may skip normal handlers. + + If the tool successfully handles the request (by setting response.body), + if should return True. This will cause CherryPy to skip any 'normal' page + handler. If the tool did not handle the request, it should return False + to tell CherryPy to continue on and call the normal page handler. If the + tool is declared AS a page handler (see the 'handler' method), returning + False will raise NotFound. + """ + + def __init__(self, callable, name=None): + Tool.__init__(self, 'before_handler', callable, name) + + def handler(self, *args, **kwargs): + """Use this tool as a CherryPy page handler. + + For example:: + + class Root: + nav = tools.staticdir.handler(section="/nav", dir="nav", + root=absDir) + """ + @expose + def handle_func(*a, **kw): + handled = self.callable(*args, **self._merged_args(kwargs)) + if not handled: + raise cherrypy.NotFound() + return cherrypy.serving.response.body + return handle_func + + def _wrapper(self, **kwargs): + if self.callable(**kwargs): + cherrypy.serving.request.handler = None + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self._wrapper, + priority=p, **conf) + + +class HandlerWrapperTool(Tool): + + """Tool which wraps request.handler in a provided wrapper function. + + The 'newhandler' arg must be a handler wrapper function that takes a + 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all + page handler + functions, it must return an iterable for use as cherrypy.response.body. + + For example, to allow your 'inner' page handlers to return dicts + which then get interpolated into a template:: + + def interpolator(next_handler, *args, **kwargs): + filename = cherrypy.request.config.get('template') + cherrypy.response.template = env.get_template(filename) + response_dict = next_handler(*args, **kwargs) + return cherrypy.response.template.render(**response_dict) + cherrypy.tools.jinja = HandlerWrapperTool(interpolator) + """ + + def __init__(self, newhandler, point='before_handler', name=None, + priority=50): + self.newhandler = newhandler + self._point = point + self._name = name + self._priority = priority + + def callable(self, *args, **kwargs): + innerfunc = cherrypy.serving.request.handler + + def wrap(*args, **kwargs): + return self.newhandler(innerfunc, *args, **kwargs) + cherrypy.serving.request.handler = wrap + + +class ErrorTool(Tool): + + """Tool which is used to replace the default request.error_response.""" + + def __init__(self, callable, name=None): + Tool.__init__(self, None, callable, name) + + def _wrapper(self): + self.callable(**self._merged_args()) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + cherrypy.serving.request.error_response = self._wrapper + + +# Builtin tools # + + +class SessionTool(Tool): + + """Session Tool for CherryPy. + + sessions.locking + When 'implicit' (the default), the session will be locked for you, + just before running the page handler. + + When 'early', the session will be locked before reading the request + body. This is off by default for safety reasons; for example, + a large upload would block the session, denying an AJAX + progress meter + (`issue `_). + + When 'explicit' (or any other value), you need to call + cherrypy.session.acquire_lock() yourself before using + session data. + """ + + def __init__(self): + # _sessions.init must be bound after headers are read + Tool.__init__(self, 'before_request_body', _sessions.init) + + def _lock_session(self): + cherrypy.serving.session.acquire_lock() + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + hooks = cherrypy.serving.request.hooks + + conf = self._merged_args() + + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + + hooks.attach(self._point, self.callable, priority=p, **conf) + + locking = conf.pop('locking', 'implicit') + if locking == 'implicit': + hooks.attach('before_handler', self._lock_session) + elif locking == 'early': + # Lock before the request body (but after _sessions.init runs!) + hooks.attach('before_request_body', self._lock_session, + priority=60) + else: + # Don't lock + pass + + hooks.attach('before_finalize', _sessions.save) + hooks.attach('on_end_request', _sessions.close) + + def regenerate(self): + """Drop the current session and make a new one (with a new id).""" + sess = cherrypy.serving.session + sess.regenerate() + + # Grab cookie-relevant tool args + relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure' + conf = dict( + (k, v) + for k, v in self._merged_args().items() + if k in relevant + ) + _sessions.set_response_cookie(**conf) + + +class XMLRPCController(object): + + """A Controller (page handler collection) for XML-RPC. + + To use it, have your controllers subclass this base class (it will + turn on the tool for you). + + You can also supply the following optional config entries:: + + tools.xmlrpc.encoding: 'utf-8' + tools.xmlrpc.allow_none: 0 + + XML-RPC is a rather discontinuous layer over HTTP; dispatching to the + appropriate handler must first be performed according to the URL, and + then a second dispatch step must take place according to the RPC method + specified in the request body. It also allows a superfluous "/RPC2" + prefix in the URL, supplies its own handler args in the body, and + requires a 200 OK "Fault" response instead of 404 when the desired + method is not found. + + Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. + This Controller acts as the dispatch target for the first half (based + on the URL); it then reads the RPC method from the request body and + does its own second dispatch step based on that method. It also reads + body params, and returns a Fault on error. + + The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 + in your URL's, you can safely skip turning on the XMLRPCDispatcher. + Otherwise, you need to use declare it in config:: + + request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() + """ + + # Note we're hard-coding this into the 'tools' namespace. We could do + # a huge amount of work to make it relocatable, but the only reason why + # would be if someone actually disabled the default_toolbox. Meh. + _cp_config = {'tools.xmlrpc.on': True} + + @expose + def default(self, *vpath, **params): + rpcparams, rpcmethod = _xmlrpc.process_body() + + subhandler = self + for attr in str(rpcmethod).split('.'): + subhandler = getattr(subhandler, attr, None) + + if subhandler and getattr(subhandler, 'exposed', False): + body = subhandler(*(vpath + rpcparams), **params) + + else: + # https://github.com/cherrypy/cherrypy/issues/533 + # if a method is not found, an xmlrpclib.Fault should be returned + # raising an exception here will do that; see + # cherrypy.lib.xmlrpcutil.on_error + raise Exception('method "%s" is not supported' % attr) + + conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {}) + _xmlrpc.respond(body, + conf.get('encoding', 'utf-8'), + conf.get('allow_none', 0)) + return cherrypy.serving.response.body + + +class SessionAuthTool(HandlerTool): + pass + + +class CachingTool(Tool): + + """Caching Tool for CherryPy.""" + + def _wrapper(self, **kwargs): + request = cherrypy.serving.request + if _caching.get(**kwargs): + request.handler = None + else: + if request.cacheable: + # Note the devious technique here of adding hooks on the fly + request.hooks.attach('before_finalize', _caching.tee_output, + priority=100) + _wrapper.priority = 90 + + def _setup(self): + """Hook caching into cherrypy.request.""" + conf = self._merged_args() + + p = conf.pop('priority', None) + cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, + priority=p, **conf) + + +class Toolbox(object): + + """A collection of Tools. + + This object also functions as a config namespace handler for itself. + Custom toolboxes should be added to each Application's toolboxes dict. + """ + + def __init__(self, namespace): + self.namespace = namespace + + def __setattr__(self, name, value): + # If the Tool._name is None, supply it from the attribute name. + if isinstance(value, Tool): + if value._name is None: + value._name = name + value.namespace = self.namespace + object.__setattr__(self, name, value) + + def __enter__(self): + """Populate request.toolmaps from tools specified in config.""" + cherrypy.serving.request.toolmaps[self.namespace] = map = {} + + def populate(k, v): + toolname, arg = k.split('.', 1) + bucket = map.setdefault(toolname, {}) + bucket[arg] = v + return populate + + def __exit__(self, exc_type, exc_val, exc_tb): + """Run tool._setup() for each tool in our toolmap.""" + map = cherrypy.serving.request.toolmaps.get(self.namespace) + if map: + for name, settings in map.items(): + if settings.get('on', False): + tool = getattr(self, name) + tool._setup() + + def register(self, point, **kwargs): + """ + Return a decorator which registers the function + at the given hook point. + """ + def decorator(func): + attr_name = kwargs.get('name', func.__name__) + tool = Tool(point, func, **kwargs) + setattr(self, attr_name, tool) + return func + return decorator + + +default_toolbox = _d = Toolbox('tools') +_d.session_auth = SessionAuthTool(cptools.session_auth) +_d.allow = Tool('on_start_resource', cptools.allow) +_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) +_d.response_headers = Tool('on_start_resource', cptools.response_headers) +_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) +_d.log_headers = Tool('before_error_response', cptools.log_request_headers) +_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) +_d.err_redirect = ErrorTool(cptools.redirect) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) +_d.decode = Tool('before_request_body', encoding.decode) +# the order of encoding, gzip, caching is important +_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) +_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) +_d.staticdir = HandlerTool(static.staticdir) +_d.staticfile = HandlerTool(static.staticfile) +_d.sessions = SessionTool() +_d.xmlrpc = ErrorTool(_xmlrpc.on_error) +_d.caching = CachingTool('before_handler', _caching.get, 'caching') +_d.expires = Tool('before_finalize', _caching.expires) +_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) +_d.referer = Tool('before_request_body', cptools.referer) +_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) +_d.flatten = Tool('before_finalize', cptools.flatten) +_d.accept = Tool('on_start_resource', cptools.accept) +_d.redirect = Tool('on_start_resource', cptools.redirect) +_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) +_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) +_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) +_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) +_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) +_d.params = Tool('before_handler', cptools.convert_params, priority=15) + +del _d, cptools, encoding, static diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptree.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptree.py new file mode 100644 index 000000000..917c5b1aa --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cptree.py @@ -0,0 +1,302 @@ +"""CherryPy Application and Tree objects.""" + +import os + +import cherrypy +from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools +from cherrypy.lib import httputil, reprconf + + +class Application(object): + """A CherryPy Application. + + Servers and gateways should not instantiate Request objects directly. + Instead, they should ask an Application object for a request object. + + An instance of this class may also be used as a WSGI callable + (WSGI application object) for itself. + """ + + root = None + """The top-most container of page handlers for this app. Handlers should + be arranged in a hierarchy of attributes, matching the expected URI + hierarchy; the default dispatcher then searches this hierarchy for a + matching handler. When using a dispatcher other than the default, + this value may be None.""" + + config = {} + """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict + of {key: value} pairs.""" + + namespaces = reprconf.NamespaceSet() + toolboxes = {'tools': cherrypy.tools} + + log = None + """A LogManager instance. See _cplogging.""" + + wsgiapp = None + """A CPWSGIApp instance. See _cpwsgi.""" + + request_class = _cprequest.Request + response_class = _cprequest.Response + + relative_urls = False + + def __init__(self, root, script_name='', config=None): + """Initialize Application with given root.""" + self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) + self.root = root + self.script_name = script_name + self.wsgiapp = _cpwsgi.CPWSGIApp(self) + + self.namespaces = self.namespaces.copy() + self.namespaces['log'] = lambda k, v: setattr(self.log, k, v) + self.namespaces['wsgi'] = self.wsgiapp.namespace_handler + + self.config = self.__class__.config.copy() + if config: + self.merge(config) + + def __repr__(self): + """Generate a representation of the Application instance.""" + return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__, + self.root, self.script_name) + + script_name_doc = """The URI "mount point" for this app. A mount point + is that portion of the URI which is constant for all URIs that are + serviced by this application; it does not include scheme, host, or proxy + ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + + @property + def script_name(self): # noqa: D401; irrelevant for properties + """The URI "mount point" for this app. + + A mount point is that portion of the URI which is constant for all URIs + that are serviced by this application; it does not include scheme, + host, or proxy ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + if self._script_name is not None: + return self._script_name + + # A `_script_name` with a value of None signals that the script name + # should be pulled from WSGI environ. + return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/') + + @script_name.setter + def script_name(self, value): + if value: + value = value.rstrip('/') + self._script_name = value + + def merge(self, config): + """Merge the given config into self.config.""" + _cpconfig.merge(self.config, config) + + # Handle namespaces specified in config. + self.namespaces(self.config.get('/', {})) + + def find_config(self, path, key, default=None): + """Return the most-specific value for key along path, or default.""" + trail = path or '/' + while trail: + nodeconf = self.config.get(trail, {}) + + if key in nodeconf: + return nodeconf[key] + + lastslash = trail.rfind('/') + if lastslash == -1: + break + elif lastslash == 0 and trail != '/': + trail = '/' + else: + trail = trail[:lastslash] + + return default + + def get_serving(self, local, remote, scheme, sproto): + """Create and return a Request and Response object.""" + req = self.request_class(local, remote, scheme, sproto) + req.app = self + + for name, toolbox in self.toolboxes.items(): + req.namespaces[name] = toolbox + + resp = self.response_class() + cherrypy.serving.load(req, resp) + cherrypy.engine.publish('acquire_thread') + cherrypy.engine.publish('before_request') + + return req, resp + + def release_serving(self): + """Release the current serving (request and response).""" + req = cherrypy.serving.request + + cherrypy.engine.publish('after_request') + + try: + req.close() + except Exception: + cherrypy.log(traceback=True, severity=40) + + cherrypy.serving.clear() + + def __call__(self, environ, start_response): + """Call a WSGI-callable.""" + return self.wsgiapp(environ, start_response) + + +class Tree(object): + """A registry of CherryPy applications, mounted at diverse points. + + An instance of this class may also be used as a WSGI callable + (WSGI application object), in which case it dispatches to all + mounted apps. + """ + + apps = {} + """ + A dict of the form {script name: application}, where "script name" + is a string declaring the URI mount point (no trailing slash), and + "application" is an instance of cherrypy.Application (or an arbitrary + WSGI callable if you happen to be using a WSGI server).""" + + def __init__(self): + """Initialize registry Tree.""" + self.apps = {} + + def mount(self, root, script_name='', config=None): + """Mount a new app from a root object, script_name, and config. + + root + An instance of a "controller class" (a collection of page + handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. + + script_name + A string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the + URL at which to mount the given root. For example, if root.index() + will handle requests to "http://www.example.com:8080/dept/app1/", + then the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the + root of the URI, it MUST be an empty string (not "/"). + + config + A file or dict containing application config. + """ + if script_name is None: + raise TypeError( + "The 'script_name' argument may not be None. Application " + 'objects may, however, possess a script_name of None (in ' + 'order to inpect the WSGI environ for SCRIPT_NAME upon each ' + 'request). You cannot mount such Applications on this Tree; ' + 'you must pass them to a WSGI server interface directly.') + + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + + if isinstance(root, Application): + app = root + if script_name != '' and script_name != app.script_name: + raise ValueError( + 'Cannot specify a different script name and pass an ' + 'Application instance to cherrypy.mount') + script_name = app.script_name + else: + app = Application(root, script_name) + + # If mounted at "", add favicon.ico + needs_favicon = ( + script_name == '' + and root is not None + and not hasattr(root, 'favicon_ico') + ) + if needs_favicon: + favicon = os.path.join( + os.getcwd(), + os.path.dirname(__file__), + 'favicon.ico', + ) + root.favicon_ico = tools.staticfile.handler(favicon) + + if config: + app.merge(config) + + self.apps[script_name] = app + + return app + + def graft(self, wsgi_callable, script_name=''): + """Mount a wsgi callable at the given script_name.""" + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + self.apps[script_name] = wsgi_callable + + def script_name(self, path=None): + """Return the script_name of the app at the given path, or None. + + If path is None, cherrypy.request is used. + """ + if path is None: + try: + request = cherrypy.serving.request + path = httputil.urljoin(request.script_name, + request.path_info) + except AttributeError: + return None + + while True: + if path in self.apps: + return path + + if path == '': + return None + + # Move one node up the tree and try again. + path = path[:path.rfind('/')] + + def __call__(self, environ, start_response): + """Pre-initialize WSGI env and call WSGI-callable.""" + # If you're calling this, then you're probably setting SCRIPT_NAME + # to '' (some WSGI servers always set SCRIPT_NAME to ''). + # Try to look up the app using the full path. + env1x = environ + path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), + env1x.get('PATH_INFO', '')) + sn = self.script_name(path or '/') + if sn is None: + start_response('404 Not Found', []) + return [] + + app = self.apps[sn] + + # Correct the SCRIPT_NAME and PATH_INFO environ entries. + environ = environ.copy() + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip('/')):] + return app(environ, start_response) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi.py new file mode 100644 index 000000000..b4f55fd6a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi.py @@ -0,0 +1,451 @@ +"""WSGI interface (see PEP 333 and 3333). + +Note that WSGI environ keys and values are 'native strings'; that is, +whatever the type of "" is. For Python 2, that's a byte string; for Python 3, +it's a unicode string. But PEP 3333 says: "even if Python's str type is +actually Unicode "under the hood", the content of native strings must +still be translatable to bytes via the Latin-1 encoding!" +""" + +import sys as _sys +import io + +import cherrypy as _cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cperror +from cherrypy.lib import httputil +from cherrypy.lib import is_closable_iterator + + +def downgrade_wsgi_ux_to_1x(environ): + """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. + """ + env1x = {} + + url_encoding = environ[ntou('wsgi.url_encoding')] + for k, v in environ.copy().items(): + if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: + v = v.encode(url_encoding) + elif isinstance(v, str): + v = v.encode('ISO-8859-1') + env1x[k.encode('ISO-8859-1')] = v + + return env1x + + +class VirtualHost(object): + + """Select a different WSGI application based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different applications. For example:: + + root = Root() + RootApp = cherrypy.Application(root) + Domain2App = cherrypy.Application(root) + SecureApp = cherrypy.Application(Secure()) + + vhost = cherrypy._cpwsgi.VirtualHost( + RootApp, + domains={ + 'www.domain2.example': Domain2App, + 'www.domain2.example:443': SecureApp, + }, + ) + + cherrypy.tree.graft(vhost) + """ + default = None + """Required. The default WSGI application.""" + + use_x_forwarded_host = True + """If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying.""" + + domains = {} + """A dict of {host header value: application} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding WSGI application + will be called instead of the default. Note that you often need + separate entries for "example.com" and "www.example.com". + In addition, "Host" headers may contain the port number. + """ + + def __init__(self, default, domains=None, use_x_forwarded_host=True): + self.default = default + self.domains = domains or {} + self.use_x_forwarded_host = use_x_forwarded_host + + def __call__(self, environ, start_response): + domain = environ.get('HTTP_HOST', '') + if self.use_x_forwarded_host: + domain = environ.get('HTTP_X_FORWARDED_HOST', domain) + + nextapp = self.domains.get(domain) + if nextapp is None: + nextapp = self.default + return nextapp(environ, start_response) + + +class InternalRedirector(object): + + """WSGI middleware that handles raised cherrypy.InternalRedirect.""" + + def __init__(self, nextapp, recursive=False): + self.nextapp = nextapp + self.recursive = recursive + + def __call__(self, environ, start_response): + redirections = [] + while True: + environ = environ.copy() + try: + return self.nextapp(environ, start_response) + except _cherrypy.InternalRedirect: + ir = _sys.exc_info()[1] + sn = environ.get('SCRIPT_NAME', '') + path = environ.get('PATH_INFO', '') + qs = environ.get('QUERY_STRING', '') + + # Add the *previous* path_info + qs to redirections. + old_uri = sn + path + if qs: + old_uri += '?' + qs + redirections.append(old_uri) + + if not self.recursive: + # Check to see if the new URI has been redirected to + # already + new_uri = sn + ir.path + if ir.query_string: + new_uri += '?' + ir.query_string + if new_uri in redirections: + ir.request.close() + tmpl = ( + 'InternalRedirector visited the same URL twice: %r' + ) + raise RuntimeError(tmpl % new_uri) + + # Munge the environment and try again. + environ['REQUEST_METHOD'] = 'GET' + environ['PATH_INFO'] = ir.path + environ['QUERY_STRING'] = ir.query_string + environ['wsgi.input'] = io.BytesIO() + environ['CONTENT_LENGTH'] = '0' + environ['cherrypy.previous_request'] = ir.request + + +class ExceptionTrapper(object): + + """WSGI middleware that traps exceptions.""" + + def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): + self.nextapp = nextapp + self.throws = throws + + def __call__(self, environ, start_response): + return _TrappedResponse( + self.nextapp, + environ, + start_response, + self.throws + ) + + +class _TrappedResponse(object): + + response = iter([]) + + def __init__(self, nextapp, environ, start_response, throws): + self.nextapp = nextapp + self.environ = environ + self.start_response = start_response + self.throws = throws + self.started_response = False + self.response = self.trap( + self.nextapp, self.environ, self.start_response, + ) + self.iter_response = iter(self.response) + + def __iter__(self): + self.started_response = True + return self + + def __next__(self): + return self.trap(next, self.iter_response) + + def close(self): + if hasattr(self.response, 'close'): + self.response.close() + + def trap(self, func, *args, **kwargs): + try: + return func(*args, **kwargs) + except self.throws: + raise + except StopIteration: + raise + except Exception: + tb = _cperror.format_exc() + _cherrypy.log(tb, severity=40) + if not _cherrypy.request.show_tracebacks: + tb = '' + s, h, b = _cperror.bare_error(tb) + if True: + # What fun. + s = s.decode('ISO-8859-1') + h = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in h + ] + if self.started_response: + # Empty our iterable (so future calls raise StopIteration) + self.iter_response = iter([]) + else: + self.iter_response = iter(b) + + try: + self.start_response(s, h, _sys.exc_info()) + except Exception: + # "The application must not trap any exceptions raised by + # start_response, if it called start_response with exc_info. + # Instead, it should allow such exceptions to propagate + # back to the server or gateway." + # But we still log and call close() to clean up ourselves. + _cherrypy.log(traceback=True, severity=40) + raise + + if self.started_response: + return b''.join(b) + else: + return b + + +# WSGI-to-CP Adapter # + + +class AppResponse(object): + + """WSGI response iterable for CherryPy applications.""" + + def __init__(self, environ, start_response, cpapp): + self.cpapp = cpapp + try: + self.environ = environ + self.run() + + r = _cherrypy.serving.response + + outstatus = r.output_status + if not isinstance(outstatus, bytes): + raise TypeError('response.output_status is not a byte string.') + + outheaders = [] + for k, v in r.header_list: + if not isinstance(k, bytes): + tmpl = 'response.header_list key %r is not a byte string.' + raise TypeError(tmpl % k) + if not isinstance(v, bytes): + tmpl = ( + 'response.header_list value %r is not a byte string.' + ) + raise TypeError(tmpl % v) + outheaders.append((k, v)) + + if True: + # According to PEP 3333, when using Python 3, the response + # status and headers must be bytes masquerading as unicode; + # that is, they must be of type "str" but are restricted to + # code points in the "latin-1" set. + outstatus = outstatus.decode('ISO-8859-1') + outheaders = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in outheaders + ] + + self.iter_response = iter(r.body) + self.write = start_response(outstatus, outheaders) + except BaseException: + self.close() + raise + + def __iter__(self): + return self + + def __next__(self): + return next(self.iter_response) + + def close(self): + """Close and de-reference the current request and response. (Core)""" + streaming = _cherrypy.serving.response.stream + self.cpapp.release_serving() + + # We avoid the expense of examining the iterator to see if it's + # closable unless we are streaming the response, as that's the + # only situation where we are going to have an iterator which + # may not have been exhausted yet. + if streaming and is_closable_iterator(self.iter_response): + iter_close = self.iter_response.close + try: + iter_close() + except Exception: + _cherrypy.log(traceback=True, severity=40) + + def run(self): + """Create a Request object using environ.""" + env = self.environ.get + + local = httputil.Host( + '', + int(env('SERVER_PORT', 80) or -1), + env('SERVER_NAME', ''), + ) + remote = httputil.Host( + env('REMOTE_ADDR', ''), + int(env('REMOTE_PORT', -1) or -1), + env('REMOTE_HOST', ''), + ) + scheme = env('wsgi.url_scheme') + sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1') + request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) + + # LOGON_USER is served by IIS, and is the name of the + # user after having been mapped to a local account. + # Both IIS and Apache set REMOTE_USER, when possible. + request.login = env('LOGON_USER') or env('REMOTE_USER') or None + request.multithread = self.environ['wsgi.multithread'] + request.multiprocess = self.environ['wsgi.multiprocess'] + request.wsgi_environ = self.environ + request.prev = env('cherrypy.previous_request', None) + + meth = self.environ['REQUEST_METHOD'] + + path = httputil.urljoin( + self.environ.get('SCRIPT_NAME', ''), + self.environ.get('PATH_INFO', ''), + ) + qs = self.environ.get('QUERY_STRING', '') + + path, qs = self.recode_path_qs(path, qs) or (path, qs) + + rproto = self.environ.get('SERVER_PROTOCOL') + headers = self.translate_headers(self.environ) + rfile = self.environ['wsgi.input'] + request.run(meth, path, qs, rproto, headers, rfile) + + headerNames = { + 'HTTP_CGI_AUTHORIZATION': 'Authorization', + 'CONTENT_LENGTH': 'Content-Length', + 'CONTENT_TYPE': 'Content-Type', + 'REMOTE_HOST': 'Remote-Host', + 'REMOTE_ADDR': 'Remote-Addr', + } + + def recode_path_qs(self, path, qs): + # This isn't perfect; if the given PATH_INFO is in the + # wrong encoding, it may fail to match the appropriate config + # section URI. But meh. + old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') + new_enc = self.cpapp.find_config( + self.environ.get('PATH_INFO', ''), + 'request.uri_encoding', 'utf-8', + ) + if new_enc.lower() == old_enc.lower(): + return + + # Even though the path and qs are unicode, the WSGI server + # is required by PEP 3333 to coerce them to ISO-8859-1 + # masquerading as unicode. So we have to encode back to + # bytes and then decode again using the "correct" encoding. + try: + return ( + path.encode(old_enc).decode(new_enc), + qs.encode(old_enc).decode(new_enc), + ) + except (UnicodeEncodeError, UnicodeDecodeError): + # Just pass them through without transcoding and hope. + pass + + def translate_headers(self, environ): + """Translate CGI-environ header names to HTTP header names.""" + for cgiName in environ: + # We assume all incoming header keys are uppercase already. + if cgiName in self.headerNames: + yield self.headerNames[cgiName], environ[cgiName] + elif cgiName[:5] == 'HTTP_': + # Hackish attempt at recovering original header names. + translatedHeader = cgiName[5:].replace('_', '-') + yield translatedHeader, environ[cgiName] + + +class CPWSGIApp(object): + + """A WSGI application object for a CherryPy Application.""" + + pipeline = [ + ('ExceptionTrapper', ExceptionTrapper), + ('InternalRedirector', InternalRedirector), + ] + """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a + constructor that takes an initial, positional 'nextapp' argument, + plus optional keyword arguments, and returns a WSGI application + (that takes environ and start_response arguments). The 'name' can + be any you choose, and will correspond to keys in self.config.""" + + head = None + """Rather than nest all apps in the pipeline on each call, it's only + done the first time, and the result is memoized into self.head. Set + this to None again if you change self.pipeline after calling self.""" + + config = {} + """A dict whose keys match names listed in the pipeline. Each + value is a further dict which will be passed to the corresponding + named WSGI callable (from the pipeline) as keyword arguments.""" + + response_class = AppResponse + """The class to instantiate and return as the next app in the WSGI chain. + """ + + def __init__(self, cpapp, pipeline=None): + self.cpapp = cpapp + self.pipeline = self.pipeline[:] + if pipeline: + self.pipeline.extend(pipeline) + self.config = self.config.copy() + + def tail(self, environ, start_response): + """WSGI application callable for the actual CherryPy application. + + You probably shouldn't call this; call self.__call__ instead, + so that any WSGI middleware in self.pipeline can run first. + """ + return self.response_class(environ, start_response, self.cpapp) + + def __call__(self, environ, start_response): + head = self.head + if head is None: + # Create and nest the WSGI apps in our pipeline (in reverse order). + # Then memoize the result in self.head. + head = self.tail + for name, callable in self.pipeline[::-1]: + conf = self.config.get(name, {}) + head = callable(head, **conf) + self.head = head + return head(environ, start_response) + + def namespace_handler(self, k, v): + """Config handler for the 'wsgi' namespace.""" + if k == 'pipeline': + # Note this allows multiple 'wsgi.pipeline' config entries + # (but each entry will be processed in a 'random' order). + # It should also allow developers to set default middleware + # in code (passed to self.__init__) that deployers can add to + # (but not remove) via config. + self.pipeline.extend(v) + elif k == 'response_class': + self.response_class = v + else: + name, arg = k.split('.', 1) + bucket = self.config.setdefault(name, {}) + bucket[arg] = v diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi_server.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi_server.py new file mode 100644 index 000000000..11dd846af --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_cpwsgi_server.py @@ -0,0 +1,110 @@ +""" +WSGI server interface (see PEP 333). + +This adds some CP-specific bits to the framework-agnostic cheroot package. +""" +import sys + +import cheroot.wsgi +import cheroot.server + +import cherrypy + + +class CPWSGIHTTPRequest(cheroot.server.HTTPRequest): + """Wrapper for cheroot.server.HTTPRequest. + + This is a layer, which preserves URI parsing mode like it which was + before Cheroot v5.8.0. + """ + + def __init__(self, server, conn): + """Initialize HTTP request container instance. + + Args: + server (cheroot.server.HTTPServer): + web server object receiving this request + conn (cheroot.server.HTTPConnection): + HTTP connection object for this request + """ + super(CPWSGIHTTPRequest, self).__init__( + server, conn, proxy_mode=True + ) + + +class CPWSGIServer(cheroot.wsgi.Server): + """Wrapper for cheroot.wsgi.Server. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. Therefore, + we wrap it here, so we can set our own mount points from cherrypy.tree + and apply some attributes from config -> cherrypy.server -> wsgi.Server. + """ + + fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' + version = fmt.format(**globals()) + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPWSGIServer instance. + + Args: + server_adapter (cherrypy._cpserver.Server): ... + """ + self.server_adapter = server_adapter + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0 + ) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0 + ) + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + self.wsgi_version = self.server_adapter.wsgi_version + + super(CPWSGIServer, self).__init__( + server_adapter.bind_addr, cherrypy.tree, + self.server_adapter.thread_pool, + server_name, + max=self.server_adapter.thread_pool_max, + request_queue_size=self.server_adapter.socket_queue_size, + timeout=self.server_adapter.socket_timeout, + shutdown_timeout=self.server_adapter.shutdown_timeout, + accepted_queue_size=self.server_adapter.accepted_queue_size, + accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, + peercreds_enabled=self.server_adapter.peercreds, + peercreds_resolve_enabled=self.server_adapter.peercreds_resolve, + ) + self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest + + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + if sys.version_info >= (3, 0): + ssl_module = self.server_adapter.ssl_module or 'builtin' + else: + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + + self.stats['Enabled'] = getattr( + self.server_adapter, 'statistics', False) + + def error_log(self, msg='', level=20, traceback=False): + """Write given message to the error log.""" + cherrypy.engine.log(msg, level, traceback) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_helper.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_helper.py new file mode 100644 index 000000000..d57cd1f96 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_helper.py @@ -0,0 +1,348 @@ +"""Helper functions for CP apps.""" + +import urllib.parse + +from cherrypy._cpcompat import text_or_bytes + +import cherrypy + + +def expose(func=None, alias=None): + """Expose the function or class. + + Optionally provide an alias or set of aliases. + """ + def expose_(func): + func.exposed = True + if alias is not None: + if isinstance(alias, text_or_bytes): + parents[alias.replace('.', '_')] = func + else: + for a in alias: + parents[a.replace('.', '_')] = func + return func + + import sys + import types + decoratable_types = types.FunctionType, types.MethodType, type, + if isinstance(func, decoratable_types): + if alias is None: + # @expose + func.exposed = True + return func + else: + # func = expose(func, alias) + parents = sys._getframe(1).f_locals + return expose_(func) + elif func is None: + if alias is None: + # @expose() + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose(alias="alias") or + # @expose(alias=["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose("alias") or + # @expose(["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + alias = func + return expose_ + + +def popargs(*args, **kwargs): + """Decorate _cp_dispatch. + + (cherrypy.dispatch.Dispatcher.dispatch_method_name) + + Optional keyword argument: handler=(Object or Function) + + Provides a _cp_dispatch function that pops off path segments into + cherrypy.request.params under the names specified. The dispatch + is then forwarded on to the next vpath element. + + Note that any existing (and exposed) member function of the class that + popargs is applied to will override that value of the argument. For + instance, if you have a method named "list" on the class decorated with + popargs, then accessing "/list" will call that function instead of popping + it off as the requested parameter. This restriction applies to all + _cp_dispatch functions. The only way around this restriction is to create + a "blank class" whose only function is to provide _cp_dispatch. + + If there are path elements after the arguments, or more arguments + are requested than are available in the vpath, then the 'handler' + keyword argument specifies the next object to handle the parameterized + request. If handler is not specified or is None, then self is used. + If handler is a function rather than an instance, then that function + will be called with the args specified and the return value from that + function used as the next object INSTEAD of adding the parameters to + cherrypy.request.args. + + This decorator may be used in one of two ways: + + As a class decorator: + + .. code-block:: python + + @cherrypy.popargs('year', 'month', 'day') + class Blog: + def index(self, year=None, month=None, day=None): + #Process the parameters here; any url like + #/, /2009, /2009/12, or /2009/12/31 + #will fill in the appropriate parameters. + + def create(self): + #This link will still be available at /create. + #Defined functions take precedence over arguments. + + Or as a member of a class: + + .. code-block:: python + + class Blog: + _cp_dispatch = cherrypy.popargs('year', 'month', 'day') + #... + + The handler argument may be used to mix arguments with built in functions. + For instance, the following setup allows different activities at the + day, month, and year level: + + .. code-block:: python + + class DayHandler: + def index(self, year, month, day): + #Do something with this day; probably list entries + + def delete(self, year, month, day): + #Delete all entries for this day + + @cherrypy.popargs('day', handler=DayHandler()) + class MonthHandler: + def index(self, year, month): + #Do something with this month; probably list entries + + def delete(self, year, month): + #Delete all entries for this month + + @cherrypy.popargs('month', handler=MonthHandler()) + class YearHandler: + def index(self, year): + #Do something with this year + + #... + + @cherrypy.popargs('year', handler=YearHandler()) + class Root: + def index(self): + #... + + """ + # Since keyword arg comes after *args, we have to process it ourselves + # for lower versions of python. + + handler = None + handler_call = False + for k, v in kwargs.items(): + if k == 'handler': + handler = v + else: + tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'" + raise TypeError(tm.format(k)) + + import inspect + + if handler is not None \ + and (hasattr(handler, '__call__') or inspect.isclass(handler)): + handler_call = True + + def decorated(cls_or_self=None, vpath=None): + if inspect.isclass(cls_or_self): + # cherrypy.popargs is a class decorator + cls = cls_or_self + name = cherrypy.dispatch.Dispatcher.dispatch_method_name + setattr(cls, name, decorated) + return cls + + # We're in the actual function + self = cls_or_self + parms = {} + for arg in args: + if not vpath: + break + parms[arg] = vpath.pop(0) + + if handler is not None: + if handler_call: + return handler(**parms) + else: + cherrypy.request.params.update(parms) + return handler + + cherrypy.request.params.update(parms) + + # If we are the ultimate handler, then to prevent our _cp_dispatch + # from being called again, we will resolve remaining elements through + # getattr() directly. + if vpath: + return getattr(self, vpath.pop(0), None) + else: + return self + + return decorated + + +def url(path='', qs='', script_name=None, base=None, relative=None): + """Create an absolute URL for the given path. + + If 'path' starts with a slash ('/'), this will return + (base + script_name + path + qs). + If it does not start with a slash, this returns + (base + script_name [+ request.path_info] + path + qs). + + If script_name is None, cherrypy.request will be used + to find a script_name, if available. + + If base is None, cherrypy.request.base will be used (if available). + Note that you can use cherrypy.tools.proxy to change this. + + Finally, note that this function can be used to obtain an absolute URL + for the current request path (minus the querystring) by passing no args. + If you call url(qs=cherrypy.request.query_string), you should get the + original browser URL (assuming no internal redirections). + + If relative is None or not provided, request.app.relative_urls will + be used (if available, else False). If False, the output will be an + absolute URL (including the scheme, host, vhost, and script_name). + If True, the output will instead be a URL that is relative to the + current request path, perhaps including '..' atoms. If relative is + the string 'server', the output will instead be a URL that is + relative to the server root; i.e., it will start with a slash. + """ + if isinstance(qs, (tuple, list, dict)): + qs = urllib.parse.urlencode(qs) + if qs: + qs = '?' + qs + + if cherrypy.request.app: + if not path.startswith('/'): + # Append/remove trailing slash from path_info as needed + # (this is to support mistyped URL's without redirecting; + # if you want to redirect, use tools.trailing_slash). + pi = cherrypy.request.path_info + if cherrypy.request.is_index is True: + if not pi.endswith('/'): + pi = pi + '/' + elif cherrypy.request.is_index is False: + if pi.endswith('/') and pi != '/': + pi = pi[:-1] + + if path == '': + path = pi + else: + path = urllib.parse.urljoin(pi, path) + + if script_name is None: + script_name = cherrypy.request.script_name + if base is None: + base = cherrypy.request.base + + newurl = base + script_name + normalize_path(path) + qs + else: + # No request.app (we're being called outside a request). + # We'll have to guess the base from server.* attributes. + # This will produce very different results from the above + # if you're using vhosts or tools.proxy. + if base is None: + base = cherrypy.server.base() + + path = (script_name or '') + path + newurl = base + normalize_path(path) + qs + + # At this point, we should have a fully-qualified absolute URL. + + if relative is None: + relative = getattr(cherrypy.request.app, 'relative_urls', False) + + # See http://www.ietf.org/rfc/rfc2396.txt + if relative == 'server': + # "A relative reference beginning with a single slash character is + # termed an absolute-path reference, as defined by ..." + # This is also sometimes called "server-relative". + newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) + elif relative: + # "A relative reference that does not begin with a scheme name + # or a slash character is termed a relative-path reference." + old = url(relative=False).split('/')[:-1] + new = newurl.split('/') + while old and new: + a, b = old[0], new[0] + if a != b: + break + old.pop(0) + new.pop(0) + new = (['..'] * len(old)) + new + newurl = '/'.join(new) + + return newurl + + +def normalize_path(path): + """Resolve given path from relative into absolute form.""" + if './' not in path: + return path + + # Normalize the URL by removing ./ and ../ + atoms = [] + for atom in path.split('/'): + if atom == '.': + pass + elif atom == '..': + # Don't pop from empty list + # (i.e. ignore redundant '..') + if atoms: + atoms.pop() + elif atom: + atoms.append(atom) + + newpath = '/'.join(atoms) + # Preserve leading '/' + if path.startswith('/'): + newpath = '/' + newpath + + return newpath + + +#### +# Inlined from jaraco.classes 1.4.3 +# Ref #1673 +class _ClassPropertyDescriptor(object): + """Descript for read-only class-based property. + + Turns a classmethod-decorated func into a read-only property of that class + type (means the value cannot be set). + """ + + def __init__(self, fget, fset=None): + """Initialize a class property descriptor. + + Instantiated by ``_helper.classproperty``. + """ + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + """Return property value.""" + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + +def classproperty(func): # noqa: D401; irrelevant for properties + """Decorator like classmethod to implement a static class property.""" + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return _ClassPropertyDescriptor(func) +#### diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/_json.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/_json.py new file mode 100644 index 000000000..0c2a0f0e0 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/_json.py @@ -0,0 +1,25 @@ +""" +JSON support. + +Expose preferred json module as json and provide encode/decode +convenience functions. +""" + +try: + # Prefer simplejson + import simplejson as json +except ImportError: + import json + + +__all__ = ['json', 'encode', 'decode'] + + +decode = json.JSONDecoder().decode +_encode = json.JSONEncoder().iterencode + + +def encode(value): + """Encode to bytes.""" + for chunk in _encode(value): + yield chunk.encode('utf-8') diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/daemon.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/daemon.py new file mode 100644 index 000000000..74488c06b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/daemon.py @@ -0,0 +1,107 @@ +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec('import %s' % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error('You may only specify one of the cgi, fastcgi, and ' + 'scgi options.', 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload.on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + cls = ( + servers.FlupFCGIServer if fastcgi else + servers.FlupSCGIServer if scgi else + servers.FlupCGIServer + ) + f = cls(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except Exception: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +def run(): + """Run cherryd CLI.""" + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action='append', dest='config', + help='specify config file(s)') + p.add_option('-d', action='store_true', dest='daemonize', + help='run the server as a daemon') + p.add_option('-e', '--environment', dest='environment', default=None, + help='apply the given config environment') + p.add_option('-f', action='store_true', dest='fastcgi', + help='start a fastcgi server instead of the default HTTP ' + 'server') + p.add_option('-s', action='store_true', dest='scgi', + help='start a scgi server instead of the default HTTP server') + p.add_option('-x', action='store_true', dest='cgi', + help='start a cgi server instead of the default HTTP server') + p.add_option('-i', '--import', action='append', dest='imports', + help='specify modules to import') + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help='store the process id in the given file') + p.add_option('-P', '--Path', action='append', dest='Path', + help='add the given paths to sys.path') + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/favicon.ico b/awesome_venv/lib/python3.10/site-packages/cherrypy/favicon.ico new file mode 100644 index 000000000..f0d7e61ba Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/favicon.ico differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__init__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__init__.py new file mode 100644 index 000000000..0edaaf20c --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__init__.py @@ -0,0 +1,101 @@ +"""CherryPy Library.""" + + +def is_iterator(obj): + """Detect if the object provided implements the iterator protocol. + + (i.e. like a generator). + + This will return False for objects which are iterable, + but not iterators themselves. + """ + from types import GeneratorType + if isinstance(obj, GeneratorType): + return True + elif not hasattr(obj, '__iter__'): + return False + else: + # Types which implement the protocol must return themselves when + # invoking 'iter' upon them. + return iter(obj) is obj + + +def is_closable_iterator(obj): + """Detect if the given object is both closable and iterator.""" + # Not an iterator. + if not is_iterator(obj): + return False + + # A generator - the easiest thing to deal with. + import inspect + if inspect.isgenerator(obj): + return True + + # A custom iterator. Look for a close method... + if not (hasattr(obj, 'close') and callable(obj.close)): + return False + + # ... which doesn't require any arguments. + try: + inspect.getcallargs(obj.close) + except TypeError: + return False + else: + return True + + +class file_generator(object): + """Yield the given input (a file object) in chunks (default 64k). + + (Core) + """ + + def __init__(self, input, chunkSize=65536): + """Initialize file_generator with file ``input`` for chunked access.""" + self.input = input + self.chunkSize = chunkSize + + def __iter__(self): + """Return iterator.""" + return self + + def __next__(self): + """Return next chunk of file.""" + chunk = self.input.read(self.chunkSize) + if chunk: + return chunk + else: + if hasattr(self.input, 'close'): + self.input.close() + raise StopIteration() + next = __next__ + + def __del__(self): + """Close input on descturct.""" + if hasattr(self.input, 'close'): + self.input.close() + + +def file_generator_limited(fileobj, count, chunk_size=65536): + """Yield the given file object in chunks. + + Stopps after `count` bytes has been emitted. + Default chunk size is 64kB. (Core) + """ + remaining = count + while remaining > 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + + +def set_vary_header(response, header_name): + """Add a Vary header to a response.""" + varies = response.headers.get('Vary', '') + varies = [x.strip() for x in varies.split(',') if x.strip()] + if header_name not in varies: + varies.append(header_name) + response.headers['Vary'] = ', '.join(varies) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..beea3f809 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_basic.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_basic.cpython-310.pyc new file mode 100644 index 000000000..5ea47dc76 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_basic.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_digest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_digest.cpython-310.pyc new file mode 100644 index 000000000..5cc3231b9 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/auth_digest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/caching.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/caching.cpython-310.pyc new file mode 100644 index 000000000..5775abe79 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/caching.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/covercp.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/covercp.cpython-310.pyc new file mode 100644 index 000000000..bd0f596c6 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/covercp.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cpstats.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cpstats.cpython-310.pyc new file mode 100644 index 000000000..5539abd77 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cpstats.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cptools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cptools.cpython-310.pyc new file mode 100644 index 000000000..38b579051 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/cptools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/encoding.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/encoding.cpython-310.pyc new file mode 100644 index 000000000..052c751de Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/encoding.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/gctools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/gctools.cpython-310.pyc new file mode 100644 index 000000000..1b70eb218 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/gctools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/httputil.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/httputil.cpython-310.pyc new file mode 100644 index 000000000..4ea998365 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/httputil.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/jsontools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/jsontools.cpython-310.pyc new file mode 100644 index 000000000..fc26794a6 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/jsontools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/locking.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/locking.cpython-310.pyc new file mode 100644 index 000000000..625a9fdf7 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/locking.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/profiler.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/profiler.cpython-310.pyc new file mode 100644 index 000000000..3fad7f2d6 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/profiler.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/reprconf.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/reprconf.cpython-310.pyc new file mode 100644 index 000000000..dbb529877 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/reprconf.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/sessions.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/sessions.cpython-310.pyc new file mode 100644 index 000000000..fcfebeff5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/sessions.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/static.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/static.cpython-310.pyc new file mode 100644 index 000000000..6571aef9c Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/static.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/xmlrpcutil.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/xmlrpcutil.cpython-310.pyc new file mode 100644 index 000000000..bd5c7cd81 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/__pycache__/xmlrpcutil.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_basic.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_basic.py new file mode 100644 index 000000000..ad379a260 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_basic.py @@ -0,0 +1,120 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Basic Authentication tool. + +This module provides a CherryPy 3.x tool which implements +the server-side of HTTP Basic Access Authentication, as described in +:rfc:`2617`. + +Example usage, using the built-in checkpassword_dict function which uses a dict +as the credentials store:: + + userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} + checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) + basic_auth = {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'earth', + 'tools.auth_basic.checkpassword': checkpassword, + 'tools.auth_basic.accept_charset': 'UTF-8', + } + app_config = { '/' : basic_auth } + +""" + +import binascii +import unicodedata +import base64 + +import cherrypy +from cherrypy._cpcompat import ntou, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def checkpassword_dict(user_password_dict): + """Returns a checkpassword function which checks credentials + against a dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, use + checkpassword_dict(my_credentials_dict) as the value for the + checkpassword argument to basic_auth(). + """ + def checkpassword(realm, user, password): + p = user_password_dict.get(user) + return p and p == password or False + + return checkpassword + + +def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'): + """A CherryPy tool which hooks at before_handler to perform + HTTP Basic Access Authentication, as specified in :rfc:`2617` + and :rfc:`7617`. + + If the request has an 'authorization' header with a 'Basic' scheme, this + tool attempts to authenticate the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not 'Basic', or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Basic header. + + realm + A string containing the authentication realm. + + checkpassword + A callable which checks the authentication credentials. + Its signature is checkpassword(realm, username, password). where + username and password are the values obtained from the request's + 'authorization' header. If authentication succeeds, checkpassword + returns True, else it returns False. + + """ + + fallback_charset = 'ISO-8859-1' + + if '"' in realm: + raise ValueError('Realm cannot contain the " (quote) character.') + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + if auth_header is not None: + # split() error, base64.decodestring() error + msg = 'Bad Request' + with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg): + scheme, params = auth_header.split(' ', 1) + if scheme.lower() == 'basic': + charsets = accept_charset, fallback_charset + decoded_params = base64.b64decode(params.encode('ascii')) + decoded_params = _try_decode(decoded_params, charsets) + decoded_params = ntou(decoded_params) + decoded_params = unicodedata.normalize('NFC', decoded_params) + decoded_params = tonative(decoded_params) + username, password = decoded_params.split(':', 1) + if checkpassword(realm, username, password): + if debug: + cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') + request.login = username + return # successful authentication + + charset = accept_charset.upper() + charset_declaration = ( + (', charset="%s"' % charset) + if charset != fallback_charset + else '' + ) + # Respond with 401 status and a WWW-Authenticate header + cherrypy.serving.response.headers['www-authenticate'] = ( + 'Basic realm="%s"%s' % (realm, charset_declaration) + ) + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') + + +def _try_decode(subject, charsets): + for charset in charsets[:-1]: + try: + return tonative(subject, charset) + except ValueError: + pass + return tonative(subject, charsets[-1]) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_digest.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_digest.py new file mode 100644 index 000000000..981e9a5d3 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/auth_digest.py @@ -0,0 +1,462 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Digest Authentication tool. + +An implementation of the server-side of HTTP Digest Access +Authentication, which is described in :rfc:`2617`. + +Example usage, using the built-in get_ha1_dict_plain function which uses a dict +of plaintext passwords as the credentials store:: + + userpassdict = {'alice' : '4x5istwelve'} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) + digest_auth = {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'wonderland', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.accept_charset': 'UTF-8', + } + app_config = { '/' : digest_auth } +""" + +import time +import functools +from hashlib import md5 +from urllib.request import parse_http_list, parse_keqv_list + +import cherrypy +from cherrypy._cpcompat import ntob, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def md5_hex(s): + return md5(ntob(s, 'utf-8')).hexdigest() + + +qop_auth = 'auth' +qop_auth_int = 'auth-int' +valid_qops = (qop_auth, qop_auth_int) + +valid_algorithms = ('MD5', 'MD5-sess') + +FALLBACK_CHARSET = 'ISO-8859-1' +DEFAULT_CHARSET = 'UTF-8' + + +def TRACE(msg): + cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') + +# Three helper functions for users of the tool, providing three variants +# of get_ha1() functions for three different kinds of credential stores. + + +def get_ha1_dict_plain(user_password_dict): + """Returns a get_ha1 function which obtains a plaintext password from a + dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, with plaintext + passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the + get_ha1 argument to digest_auth(). + """ + def get_ha1(realm, username): + password = user_password_dict.get(username) + if password: + return md5_hex('%s:%s:%s' % (username, realm, password)) + return None + + return get_ha1 + + +def get_ha1_dict(user_ha1_dict): + """Returns a get_ha1 function which obtains a HA1 password hash from a + dictionary of the form: {username : HA1}. + + If you want a dictionary-based authentication scheme, but with + pre-computed HA1 hashes instead of plain-text passwords, use + get_ha1_dict(my_userha1_dict) as the value for the get_ha1 + argument to digest_auth(). + """ + def get_ha1(realm, username): + return user_ha1_dict.get(username) + + return get_ha1 + + +def get_ha1_file_htdigest(filename): + """Returns a get_ha1 function which obtains a HA1 password hash from a + flat file with lines of the same format as that produced by the Apache + htdigest utility. For example, for realm 'wonderland', username 'alice', + and password '4x5istwelve', the htdigest line would be:: + + alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c + + If you want to use an Apache htdigest file as the credentials store, + then use get_ha1_file_htdigest(my_htdigest_file) as the value for the + get_ha1 argument to digest_auth(). It is recommended that the filename + argument be an absolute path, to avoid problems. + """ + def get_ha1(realm, username): + result = None + with open(filename, 'r') as f: + for line in f: + u, r, ha1 = line.rstrip().split(':') + if u == username and r == realm: + result = ha1 + break + return result + + return get_ha1 + + +def synthesize_nonce(s, key, timestamp=None): + """Synthesize a nonce value which resists spoofing and can be checked + for staleness. Returns a string suitable as the value for 'nonce' in + the www-authenticate header. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + timestamp + An integer seconds-since-the-epoch timestamp + + """ + if timestamp is None: + timestamp = int(time.time()) + h = md5_hex('%s:%s:%s' % (timestamp, s, key)) + nonce = '%s:%s' % (timestamp, h) + return nonce + + +def H(s): + """The hash function H""" + return md5_hex(s) + + +def _try_decode_header(header, charset): + global FALLBACK_CHARSET + + for enc in (charset, FALLBACK_CHARSET): + try: + return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc) + except ValueError as ve: + last_err = ve + else: + raise last_err + + +class HttpDigestAuthorization(object): + """ + Parses a Digest Authorization header and performs + re-calculation of the digest. + """ + + scheme = 'digest' + + def errmsg(self, s): + return 'Digest Authorization header: %s' % s + + @classmethod + def matches(cls, header): + scheme, _, _ = header.partition(' ') + return scheme.lower() == cls.scheme + + def __init__( + self, auth_header, http_method, + debug=False, accept_charset=DEFAULT_CHARSET[:], + ): + self.http_method = http_method + self.debug = debug + + if not self.matches(auth_header): + raise ValueError('Authorization scheme is not "Digest"') + + self.auth_header = _try_decode_header(auth_header, accept_charset) + + scheme, params = self.auth_header.split(' ', 1) + + # make a dict of the params + items = parse_http_list(params) + paramsd = parse_keqv_list(items) + + self.realm = paramsd.get('realm') + self.username = paramsd.get('username') + self.nonce = paramsd.get('nonce') + self.uri = paramsd.get('uri') + self.method = paramsd.get('method') + self.response = paramsd.get('response') # the response digest + self.algorithm = paramsd.get('algorithm', 'MD5').upper() + self.cnonce = paramsd.get('cnonce') + self.opaque = paramsd.get('opaque') + self.qop = paramsd.get('qop') # qop + self.nc = paramsd.get('nc') # nonce count + + # perform some correctness checks + if self.algorithm not in valid_algorithms: + raise ValueError( + self.errmsg("Unsupported value for algorithm: '%s'" % + self.algorithm)) + + has_reqd = ( + self.username and + self.realm and + self.nonce and + self.uri and + self.response + ) + if not has_reqd: + raise ValueError( + self.errmsg('Not all required parameters are present.')) + + if self.qop: + if self.qop not in valid_qops: + raise ValueError( + self.errmsg("Unsupported value for qop: '%s'" % self.qop)) + if not (self.cnonce and self.nc): + raise ValueError( + self.errmsg('If qop is sent then ' + 'cnonce and nc MUST be present')) + else: + if self.cnonce or self.nc: + raise ValueError( + self.errmsg('If qop is not sent, ' + 'neither cnonce nor nc can be present')) + + def __str__(self): + return 'authorization : %s' % self.auth_header + + def validate_nonce(self, s, key): + """Validate the nonce. + Returns True if nonce was generated by synthesize_nonce() and the + timestamp is not spoofed, else returns False. + + s + A string related to the resource, such as the hostname of + the server. + + key + A secret string known only to the server. + + Both s and key must be the same values which were used to synthesize + the nonce we are trying to validate. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + s_timestamp, s_hashpart = synthesize_nonce( + s, key, timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + if self.debug: + TRACE('validate_nonce: %s' % is_valid) + return is_valid + except ValueError: # split() error + pass + return False + + def is_nonce_stale(self, max_age_seconds=600): + """Returns True if a validated nonce is stale. The nonce contains a + timestamp in plaintext and also a secure hash of the timestamp. + You should first validate the nonce to ensure the plaintext + timestamp is not spoofed. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + if int(timestamp) + max_age_seconds > int(time.time()): + return False + except ValueError: # int() error + pass + if self.debug: + TRACE('nonce is stale') + return True + + def HA2(self, entity_body=''): + """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" + # RFC 2617 3.2.2.3 + # If the "qop" directive's value is "auth" or is unspecified, + # then A2 is: + # A2 = method ":" digest-uri-value + # + # If the "qop" value is "auth-int", then A2 is: + # A2 = method ":" digest-uri-value ":" H(entity-body) + if self.qop is None or self.qop == 'auth': + a2 = '%s:%s' % (self.http_method, self.uri) + elif self.qop == 'auth-int': + a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) + else: + # in theory, this should never happen, since I validate qop in + # __init__() + raise ValueError(self.errmsg('Unrecognized value for qop!')) + return H(a2) + + def request_digest(self, ha1, entity_body=''): + """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. + + ha1 + The HA1 string obtained from the credentials store. + + entity_body + If 'qop' is set to 'auth-int', then A2 includes a hash + of the "entity body". The entity body is the part of the + message which follows the HTTP headers. See :rfc:`2617` section + 4.3. This refers to the entity the user agent sent in the + request which has the Authorization header. Typically GET + requests don't have an entity, and POST requests do. + + """ + ha2 = self.HA2(entity_body) + # Request-Digest -- RFC 2617 3.2.2.1 + if self.qop: + req = '%s:%s:%s:%s:%s' % ( + self.nonce, self.nc, self.cnonce, self.qop, ha2) + else: + req = '%s:%s' % (self.nonce, ha2) + + # RFC 2617 3.2.2.2 + # + # If the "algorithm" directive's value is "MD5" or is unspecified, + # then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + # + # If the "algorithm" directive's value is "MD5-sess", then A1 is + # calculated only once - on the first request by the client following + # receipt of a WWW-Authenticate challenge from the server. + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + if self.algorithm == 'MD5-sess': + ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) + + digest = H('%s:%s' % (ha1, req)) + return digest + + +def _get_charset_declaration(charset): + global FALLBACK_CHARSET + charset = charset.upper() + return ( + (', charset="%s"' % charset) + if charset != FALLBACK_CHARSET + else '' + ) + + +def www_authenticate( + realm, key, algorithm='MD5', nonce=None, qop=qop_auth, + stale=False, accept_charset=DEFAULT_CHARSET[:], +): + """Constructs a WWW-Authenticate header for Digest authentication.""" + if qop not in valid_qops: + raise ValueError("Unsupported value for qop: '%s'" % qop) + if algorithm not in valid_algorithms: + raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) + + HEADER_PATTERN = ( + 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s' + ) + + if nonce is None: + nonce = synthesize_nonce(realm, key) + + stale_param = ', stale="true"' if stale else '' + + charset_declaration = _get_charset_declaration(accept_charset) + + return HEADER_PATTERN % ( + realm, nonce, algorithm, qop, stale_param, charset_declaration, + ) + + +def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'): + """A CherryPy tool that hooks at before_handler to perform + HTTP Digest Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Digest' scheme, + this tool authenticates the credentials supplied in that header. + If the request has no 'authorization' header, or if it does but the + scheme is not "Digest", or if authentication fails, the tool sends + a 401 response with a 'WWW-Authenticate' Digest header. + + realm + A string containing the authentication realm. + + get_ha1 + A callable that looks up a username in a credentials store + and returns the HA1 string, which is defined in the RFC to be + MD5(username : realm : password). The function's signature is: + ``get_ha1(realm, username)`` + where username is obtained from the request's 'authorization' header. + If username is not found in the credentials store, get_ha1() returns + None. + + key + A secret string known only to the server, used in the synthesis + of nonces. + + """ + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + + respond_401 = functools.partial( + _respond_401, realm, key, accept_charset, debug) + + if not HttpDigestAuthorization.matches(auth_header or ''): + respond_401() + + msg = 'The Authorization header could not be parsed.' + with cherrypy.HTTPError.handle(ValueError, 400, msg): + auth = HttpDigestAuthorization( + auth_header, request.method, + debug=debug, accept_charset=accept_charset, + ) + + if debug: + TRACE(str(auth)) + + if not auth.validate_nonce(realm, key): + respond_401() + + ha1 = get_ha1(realm, auth.username) + + if ha1 is None: + respond_401() + + # note that for request.body to be available we need to + # hook in at before_handler, not on_start_resource like + # 3.1.x digest_auth does. + digest = auth.request_digest(ha1, entity_body=request.body) + if digest != auth.response: + respond_401() + + # authenticated + if debug: + TRACE('digest matches auth.response') + # Now check if nonce is stale. + # The choice of ten minutes' lifetime for nonce is somewhat + # arbitrary + if auth.is_nonce_stale(max_age_seconds=600): + respond_401(stale=True) + + request.login = auth.username + if debug: + TRACE('authentication of %s successful' % auth.username) + + +def _respond_401(realm, key, accept_charset, debug, **kwargs): + """ + Respond with 401 status and a WWW-Authenticate header + """ + header = www_authenticate( + realm, key, + accept_charset=accept_charset, + **kwargs + ) + if debug: + TRACE(header) + cherrypy.serving.response.headers['WWW-Authenticate'] = header + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/caching.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/caching.py new file mode 100644 index 000000000..08d2d8e4a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/caching.py @@ -0,0 +1,478 @@ +""" +CherryPy implements a simple caching system as a pluggable Tool. This tool +tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there +yet, but it's probably good enough for most sites. + +In general, GET responses are cached (along with selecting headers) and, if +another request arrives for the same resource, the caching Tool will return 304 +Not Modified if possible, or serve the cached response otherwise. It also sets +request.cached to True if serving a cached representation, and sets +request.cacheable to False (so it doesn't get cached again). + +If POST, PUT, or DELETE requests are made for a cached resource, they +invalidate (delete) any cached response. + +Usage +===== + +Configuration file example:: + + [/] + tools.caching.on = True + tools.caching.delay = 3600 + +You may use a class other than the default +:class:`MemoryCache` by supplying the config +entry ``cache_class``; supply the full dotted name of the replacement class +as the config value. It must implement the basic methods ``get``, ``put``, +``delete``, and ``clear``. + +You may set any attribute, including overriding methods, on the cache +instance by providing them in config. The above sets the +:attr:`delay` attribute, for example. +""" + +import datetime +import sys +import threading +import time + +import cherrypy +from cherrypy.lib import cptools, httputil + + +class Cache(object): + + """Base class for Cache implementations.""" + + def get(self): + """Return the current variant if in the cache, else None.""" + raise NotImplementedError + + def put(self, obj, size): + """Store the current variant in the cache.""" + raise NotImplementedError + + def delete(self): + """Remove ALL cached variants of the current resource.""" + raise NotImplementedError + + def clear(self): + """Reset the cache to its initial, empty state.""" + raise NotImplementedError + + +# ------------------------------ Memory Cache ------------------------------- # +class AntiStampedeCache(dict): + + """A storage system for cached items which reduces stampede collisions.""" + + def wait(self, key, timeout=5, debug=False): + """Return the cached value for the given key, or None. + + If timeout is not None, and the value is already + being calculated by another thread, wait until the given timeout has + elapsed. If the value is available before the timeout expires, it is + returned. If not, None is returned, and a sentinel placed in the cache + to signal other threads to wait. + + If timeout is None, no waiting is performed nor sentinels used. + """ + value = self.get(key) + if isinstance(value, threading.Event): + if timeout is None: + # Ignore the other thread and recalc it ourselves. + if debug: + cherrypy.log('No timeout', 'TOOLS.CACHING') + return None + + # Wait until it's done or times out. + if debug: + cherrypy.log('Waiting up to %s seconds' % + timeout, 'TOOLS.CACHING') + value.wait(timeout) + if value.result is not None: + # The other thread finished its calculation. Use it. + if debug: + cherrypy.log('Result!', 'TOOLS.CACHING') + return value.result + # Timed out. Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + + return None + elif value is None: + # Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + return value + + def __setitem__(self, key, value): + """Set the cached value for the given key.""" + existing = self.get(key) + dict.__setitem__(self, key, value) + if isinstance(existing, threading.Event): + # Set Event.result so other threads waiting on it have + # immediate access without needing to poll the cache again. + existing.result = value + existing.set() + + +class MemoryCache(Cache): + + """An in-memory cache for varying response content. + + Each key in self.store is a URI, and each value is an AntiStampedeCache. + The response for any given URI may vary based on the values of + "selecting request headers"; that is, those named in the Vary + response header. We assume the list of header names to be constant + for each URI throughout the lifetime of the application, and store + that list in ``self.store[uri].selecting_headers``. + + The items contained in ``self.store[uri]`` have keys which are tuples of + request header values (in the same order as the names in its + selecting_headers), and values which are the actual responses. + """ + + maxobjects = 1000 + """The maximum number of cached objects; defaults to 1000.""" + + maxobj_size = 100000 + """The maximum size of each cached object in bytes; defaults to 100 KB.""" + + maxsize = 10000000 + """The maximum size of the entire cache in bytes; defaults to 10 MB.""" + + delay = 600 + """Seconds until the cached content expires; defaults to 600 (10 minutes). + """ + + antistampede_timeout = 5 + """Seconds to wait for other threads to release a cache lock.""" + + expire_freq = 0.1 + """Seconds to sleep between cache expiration sweeps.""" + + debug = False + + def __init__(self): + self.clear() + + # Run self.expire_cache in a separate daemon thread. + t = threading.Thread(target=self.expire_cache, name='expire_cache') + self.expiration_thread = t + t.daemon = True + t.start() + + def clear(self): + """Reset the cache to its initial, empty state.""" + self.store = {} + self.expirations = {} + self.tot_puts = 0 + self.tot_gets = 0 + self.tot_hist = 0 + self.tot_expires = 0 + self.tot_non_modified = 0 + self.cursize = 0 + + def expire_cache(self): + """Continuously examine cached objects, expiring stale ones. + + This function is designed to be run in its own daemon thread, + referenced at ``self.expiration_thread``. + """ + # It's possible that "time" will be set to None + # arbitrarily, so we check "while time" to avoid exceptions. + # See tickets #99 and #180 for more information. + while time: + now = time.time() + # Must make a copy of expirations so it doesn't change size + # during iteration + for expiration_time, objects in self.expirations.copy().items(): + if expiration_time <= now: + for obj_size, uri, sel_header_values in objects: + try: + del self.store[uri][tuple(sel_header_values)] + self.tot_expires += 1 + self.cursize -= obj_size + except KeyError: + # the key may have been deleted elsewhere + pass + del self.expirations[expiration_time] + time.sleep(self.expire_freq) + + def get(self): + """Return the current variant if in the cache, else None.""" + request = cherrypy.serving.request + self.tot_gets += 1 + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + return None + + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + variant = uricache.wait(key=tuple(sorted(header_values)), + timeout=self.antistampede_timeout, + debug=self.debug) + if variant is not None: + self.tot_hist += 1 + return variant + + def put(self, variant, size): + """Store the current variant in the cache.""" + request = cherrypy.serving.request + response = cherrypy.serving.response + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + uricache = AntiStampedeCache() + uricache.selecting_headers = [ + e.value for e in response.headers.elements('Vary')] + self.store[uri] = uricache + + if len(self.store) < self.maxobjects: + total_size = self.cursize + size + + # checks if there's space for the object + if (size < self.maxobj_size and total_size < self.maxsize): + # add to the expirations list + expiration_time = response.time + self.delay + bucket = self.expirations.setdefault(expiration_time, []) + bucket.append((size, uri, uricache.selecting_headers)) + + # add to the cache + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + uricache[tuple(sorted(header_values))] = variant + self.tot_puts += 1 + self.cursize = total_size + + def delete(self): + """Remove ALL cached variants of the current resource.""" + uri = cherrypy.url(qs=cherrypy.serving.request.query_string) + self.store.pop(uri, None) + + +def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs): + """Try to obtain cached output. If fresh enough, raise HTTPError(304). + + If POST, PUT, or DELETE: + * invalidates (deletes) any cached response for this resource + * sets request.cached = False + * sets request.cacheable = False + + else if a cached copy exists: + * sets request.cached = True + * sets request.cacheable = False + * sets response.headers to the cached values + * checks the cached Last-Modified response header against the + current If-(Un)Modified-Since request headers; raises 304 + if necessary. + * sets response.status and response.body to the cached values + * returns True + + otherwise: + * sets request.cached = False + * sets request.cacheable = True + * returns False + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + if not hasattr(cherrypy, '_cache'): + # Make a process-wide Cache object. + cherrypy._cache = kwargs.pop('cache_class', MemoryCache)() + + # Take all remaining kwargs and set them on the Cache object. + for k, v in kwargs.items(): + setattr(cherrypy._cache, k, v) + cherrypy._cache.debug = debug + + # POST, PUT, DELETE should invalidate (delete) the cached copy. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. + if request.method in invalid_methods: + if debug: + cherrypy.log('request.method %r in invalid_methods %r' % + (request.method, invalid_methods), 'TOOLS.CACHING') + cherrypy._cache.delete() + request.cached = False + request.cacheable = False + return False + + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: + request.cached = False + request.cacheable = True + return False + + cache_data = cherrypy._cache.get() + request.cached = bool(cache_data) + request.cacheable = not request.cached + if request.cached: + # Serve the cached copy. + max_age = cherrypy._cache.delay + for v in [e.value for e in request.headers.elements('Cache-Control')]: + atoms = v.split('=', 1) + directive = atoms.pop(0) + if directive == 'max-age': + if len(atoms) != 1 or not atoms[0].isdigit(): + raise cherrypy.HTTPError( + 400, 'Invalid Cache-Control header') + max_age = int(atoms[0]) + break + elif directive == 'no-cache': + if debug: + cherrypy.log( + 'Ignoring cache due to Cache-Control: no-cache', + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + if debug: + cherrypy.log('Reading response from cache', 'TOOLS.CACHING') + s, h, b, create_time = cache_data + age = int(response.time - create_time) + if (age > max_age): + if debug: + cherrypy.log('Ignoring cache due to age > %d' % max_age, + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + # Copy the response headers. See + # https://github.com/cherrypy/cherrypy/issues/721. + response.headers = rh = httputil.HeaderMap() + for k in h: + dict.__setitem__(rh, k, dict.__getitem__(h, k)) + + # Add the required Age header + response.headers['Age'] = str(age) + + try: + # Note that validate_since depends on a Last-Modified header; + # this was put into the cached copy, and should have been + # resurrected just above (response.headers = cache_data[1]). + cptools.validate_since() + except cherrypy.HTTPRedirect: + x = sys.exc_info()[1] + if x.status == 304: + cherrypy._cache.tot_non_modified += 1 + raise + + # serve it & get out from the request + response.status = s + response.body = b + else: + if debug: + cherrypy.log('request is not cached', 'TOOLS.CACHING') + return request.cached + + +def tee_output(): + """Tee response output to cache storage. Internal.""" + # Used by CachingTool by attaching to request.hooks + + request = cherrypy.serving.request + if 'no-store' in request.headers.values('Cache-Control'): + return + + def tee(body): + """Tee response.body into a list.""" + if ('no-cache' in response.headers.values('Pragma') or + 'no-store' in response.headers.values('Cache-Control')): + for chunk in body: + yield chunk + return + + output = [] + for chunk in body: + output.append(chunk) + yield chunk + + # Save the cache data, but only if the body isn't empty. + # e.g. a 304 Not Modified on a static file response will + # have an empty body. + # If the body is empty, delete the cache because it + # contains a stale Threading._Event object that will + # stall all consecutive requests until the _Event times + # out + body = b''.join(output) + if not body: + cherrypy._cache.delete() + else: + cherrypy._cache.put((response.status, response.headers or {}, + body, response.time), len(body)) + + response = cherrypy.serving.response + response.body = tee(response.body) + + +def expires(secs=0, force=False, debug=False): + """Tool for influencing cache mechanisms using the 'Expires' header. + + secs + Must be either an int or a datetime.timedelta, and indicates the + number of seconds between response.time and when the response should + expire. The 'Expires' header will be set to response.time + secs. + If secs is zero, the 'Expires' header is set one year in the past, and + the following "cache prevention" headers are also set: + + * Pragma: no-cache + * Cache-Control': no-cache, must-revalidate + + force + If False, the following headers are checked: + + * Etag + * Last-Modified + * Age + * Expires + + If any are already present, none of the above response headers are set. + + """ + + response = cherrypy.serving.response + headers = response.headers + + cacheable = False + if not force: + # some header names that indicate that the response can be cached + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): + if indicator in headers: + cacheable = True + break + + if not cacheable and not force: + if debug: + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') + else: + if debug: + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') + if isinstance(secs, datetime.timedelta): + secs = (86400 * secs.days) + secs.seconds + + if secs == 0: + if force or ('Pragma' not in headers): + headers['Pragma'] = 'no-cache' + if cherrypy.serving.request.protocol >= (1, 1): + if force or 'Cache-Control' not in headers: + headers['Cache-Control'] = 'no-cache, must-revalidate' + # Set an explicit Expires date in the past. + expiry = httputil.HTTPDate(1169942400.0) + else: + expiry = httputil.HTTPDate(response.time + secs) + if force or 'Expires' not in headers: + headers['Expires'] = expiry diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/covercp.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/covercp.py new file mode 100644 index 000000000..6c3871fc9 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/covercp.py @@ -0,0 +1,391 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation `_ +or Ned Batchelder's `enhanced version: +`_ + +To turn on coverage tracing, use the following code:: + + cherrypy.engine.subscribe('start', covercp.start) + +DO NOT subscribe anything on the 'start_thread' channel, as previously +recommended. Calling start once in the main thread should be sufficient +to start coverage on all threads. Calling start again in each thread +effectively clears any coverage data gathered up to that point. + +Run your code, then use the ``covercp.serve()`` function to browse the +results in a web browser. If you run this module from the command line, +it will call ``serve()`` for you. +""" + +import re +import sys +import cgi +import os +import os.path +import urllib.parse + +import cherrypy + + +localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache') + +the_coverage = None +try: + from coverage import coverage + the_coverage = coverage(data_file=localFile) + + def start(): + the_coverage.start() +except ImportError: + # Setting the_coverage to None will raise errors + # that need to be trapped downstream. + the_coverage = None + + import warnings + warnings.warn( + 'No code coverage will be performed; ' + 'coverage.py could not be imported.') + + def start(): + pass +start.priority = 20 + +TEMPLATE_MENU = """ + + CherryPy Coverage Menu + + + +

CherryPy Coverage

""" + +TEMPLATE_FORM = """ +
+
+ + Show percentages +
+ Hide files over + %%
+ Exclude files matching
+ +
+ + +
+
""" + +TEMPLATE_FRAMESET = """ +CherryPy coverage data + + + + + +""" + +TEMPLATE_COVERAGE = """ + + Coverage for %(name)s + + + +

%(name)s

+

%(fullpath)s

+

Coverage: %(pc)s%%

""" + +TEMPLATE_LOC_COVERED = """ + %s  + %s +\n""" +TEMPLATE_LOC_NOT_COVERED = """ + %s  + %s +\n""" +TEMPLATE_LOC_EXCLUDED = """ + %s  + %s +\n""" + +TEMPLATE_ITEM = ( + "%s%s%s\n" +) + + +def _percent(statements, missing): + s = len(statements) + e = s - len(missing) + if s > 0: + return int(round(100.0 * e / s)) + return 0 + + +def _show_branch(root, base, path, pct=0, showpct=False, exclude='', + coverage=the_coverage): + + # Show the directory name and any of our children + dirs = [k for k, v in root.items() if v] + dirs.sort() + for name in dirs: + newpath = os.path.join(path, name) + + if newpath.lower().startswith(base): + relpath = newpath[len(base):] + yield '| ' * relpath.count(os.sep) + yield ( + "%s\n" % + (newpath, urllib.parse.quote_plus(exclude), name) + ) + + for chunk in _show_branch( + root[name], base, newpath, pct, showpct, + exclude, coverage=coverage + ): + yield chunk + + # Now list the files + if path.lower().startswith(base): + relpath = path[len(base):] + files = [k for k, v in root.items() if not v] + files.sort() + for name in files: + newpath = os.path.join(path, name) + + pc_str = '' + if showpct: + try: + _, statements, _, missing, _ = coverage.analysis2(newpath) + except Exception: + # Yes, we really want to pass on all errors. + pass + else: + pc = _percent(statements, missing) + pc_str = ('%3d%% ' % pc).replace(' ', ' ') + if pc < float(pct) or pc == -1: + pc_str = "%s" % pc_str + else: + pc_str = "%s" % pc_str + + yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1), + pc_str, newpath, name) + + +def _skip_file(path, exclude): + if exclude: + return bool(re.search(exclude, path)) + + +def _graft(path, tree): + d = tree + + p = path + atoms = [] + while True: + p, tail = os.path.split(p) + if not tail: + break + atoms.append(tail) + atoms.append(p) + if p != '/': + atoms.append('/') + + atoms.reverse() + for node in atoms: + if node: + d = d.setdefault(node, {}) + + +def get_tree(base, exclude, coverage=the_coverage): + """Return covered module names as a nested dict.""" + tree = {} + runs = coverage.data.executed_files() + for path in runs: + if not _skip_file(path, exclude) and not os.path.isdir(path): + _graft(path, tree) + return tree + + +class CoverStats(object): + + def __init__(self, coverage, root=None): + self.coverage = coverage + if root is None: + # Guess initial depth. Files outside this path will not be + # reachable from the web interface. + root = os.path.dirname(cherrypy.__file__) + self.root = root + + @cherrypy.expose + def index(self): + return TEMPLATE_FRAMESET % self.root.lower() + + @cherrypy.expose + def menu(self, base='/', pct='50', showpct='', + exclude=r'python\d\.\d|test|tut\d|tutorial'): + + # The coverage module uses all-lower-case names. + base = base.lower().rstrip(os.sep) + + yield TEMPLATE_MENU + yield TEMPLATE_FORM % locals() + + # Start by showing links for parent paths + yield "
" + path = '' + atoms = base.split(os.sep) + atoms.pop() + for atom in atoms: + path += atom + os.sep + yield ("%s %s" + % (path, urllib.parse.quote_plus(exclude), atom, os.sep)) + yield '
' + + yield "
" + + # Then display the tree + tree = get_tree(base, exclude, self.coverage) + if not tree: + yield '

No modules covered.

' + else: + for chunk in _show_branch(tree, base, '/', pct, + showpct == 'checked', exclude, + coverage=self.coverage): + yield chunk + + yield '
' + yield '' + + def annotated_file(self, filename, statements, excluded, missing): + with open(filename, 'r') as source: + lines = source.readlines() + buffer = [] + for lineno, line in enumerate(lines): + lineno += 1 + line = line.strip('\n\r') + empty_the_buffer = True + if lineno in excluded: + template = TEMPLATE_LOC_EXCLUDED + elif lineno in missing: + template = TEMPLATE_LOC_NOT_COVERED + elif lineno in statements: + template = TEMPLATE_LOC_COVERED + else: + empty_the_buffer = False + buffer.append((lineno, line)) + if empty_the_buffer: + for lno, pastline in buffer: + yield template % (lno, cgi.escape(pastline)) + buffer = [] + yield template % (lineno, cgi.escape(line)) + + @cherrypy.expose + def report(self, name): + filename, statements, excluded, missing, _ = self.coverage.analysis2( + name) + pc = _percent(statements, missing) + yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), + fullpath=name, + pc=pc) + yield '\n' + for line in self.annotated_file(filename, statements, excluded, + missing): + yield line + yield '
' + yield '' + yield '' + + +def serve(path=localFile, port=8080, root=None): + if coverage is None: + raise ImportError('The coverage module could not be imported.') + from coverage import coverage + cov = coverage(data_file=path) + cov.load() + + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': 'production', + }) + cherrypy.quickstart(CoverStats(cov, root)) + + +if __name__ == '__main__': + serve(*tuple(sys.argv[1:])) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cpstats.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cpstats.py new file mode 100644 index 000000000..111af0632 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cpstats.py @@ -0,0 +1,694 @@ +"""CPStats, a package for collecting and reporting on program statistics. + +Overview +======== + +Statistics about program operation are an invaluable monitoring and debugging +tool. Unfortunately, the gathering and reporting of these critical values is +usually ad-hoc. This package aims to add a centralized place for gathering +statistical performance data, a structure for recording that data which +provides for extrapolation of that data into more useful information, +and a method of serving that data to both human investigators and +monitoring software. Let's examine each of those in more detail. + +Data Gathering +-------------- + +Just as Python's `logging` module provides a common importable for gathering +and sending messages, performance statistics would benefit from a similar +common mechanism, and one that does *not* require each package which wishes +to collect stats to import a third-party module. Therefore, we choose to +re-use the `logging` module by adding a `statistics` object to it. + +That `logging.statistics` object is a nested dict. It is not a custom class, +because that would: + + 1. require libraries and applications to import a third-party module in + order to participate + 2. inhibit innovation in extrapolation approaches and in reporting tools, and + 3. be slow. + +There are, however, some specifications regarding the structure of the dict.:: + + { + +----"SQLAlchemy": { + | "Inserts": 4389745, + | "Inserts per Second": + | lambda s: s["Inserts"] / (time() - s["Start"]), + | C +---"Table Statistics": { + | o | "widgets": {-----------+ + N | l | "Rows": 1.3M, | Record + a | l | "Inserts": 400, | + m | e | },---------------------+ + e | c | "froobles": { + s | t | "Rows": 7845, + p | i | "Inserts": 0, + a | o | }, + c | n +---}, + e | "Slow Queries": + | [{"Query": "SELECT * FROM widgets;", + | "Processing Time": 47.840923343, + | }, + | ], + +----}, + } + +The `logging.statistics` dict has four levels. The topmost level is nothing +more than a set of names to introduce modularity, usually along the lines of +package names. If the SQLAlchemy project wanted to participate, for example, +it might populate the item `logging.statistics['SQLAlchemy']`, whose value +would be a second-layer dict we call a "namespace". Namespaces help multiple +packages to avoid collisions over key names, and make reports easier to read, +to boot. The maintainers of SQLAlchemy should feel free to use more than one +namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case +or other syntax constraints on the namespace names; they should be chosen +to be maximally readable by humans (neither too short nor too long). + +Each namespace, then, is a dict of named statistical values, such as +'Requests/sec' or 'Uptime'. You should choose names which will look +good on a report: spaces and capitalization are just fine. + +In addition to scalars, values in a namespace MAY be a (third-layer) +dict, or a list, called a "collection". For example, the CherryPy +:class:`StatsTool` keeps track of what each request is doing (or has most +recently done) in a 'Requests' collection, where each key is a thread ID; each +value in the subdict MUST be a fourth dict (whew!) of statistical data about +each thread. We call each subdict in the collection a "record". Similarly, +the :class:`StatsTool` also keeps a list of slow queries, where each record +contains data about each slow query, in order. + +Values in a namespace or record may also be functions, which brings us to: + +Extrapolation +------------- + +The collection of statistical data needs to be fast, as close to unnoticeable +as possible to the host program. That requires us to minimize I/O, for example, +but in Python it also means we need to minimize function calls. So when you +are designing your namespace and record values, try to insert the most basic +scalar values you already have on hand. + +When it comes time to report on the gathered data, however, we usually have +much more freedom in what we can calculate. Therefore, whenever reporting +tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents +of `logging.statistics` for reporting, they first call +`extrapolate_statistics` (passing the whole `statistics` dict as the only +argument). This makes a deep copy of the statistics dict so that the +reporting tool can both iterate over it and even change it without harming +the original. But it also expands any functions in the dict by calling them. +For example, you might have a 'Current Time' entry in the namespace with the +value "lambda scope: time.time()". The "scope" parameter is the current +namespace dict (or record, if we're currently expanding one of those +instead), allowing you access to existing static entries. If you're truly +evil, you can even modify more than one entry at a time. + +However, don't try to calculate an entry and then use its value in further +extrapolations; the order in which the functions are called is not guaranteed. +This can lead to a certain amount of duplicated work (or a redesign of your +schema), but that's better than complicating the spec. + +After the whole thing has been extrapolated, it's time for: + +Reporting +--------- + +The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates +it all, and then transforms it to HTML for easy viewing. Each namespace gets +its own header and attribute table, plus an extra table for each collection. +This is NOT part of the statistics specification; other tools can format how +they like. + +You can control which columns are output and how they are formatted by updating +StatsPage.formatting, which is a dict that mirrors the keys and nesting of +`logging.statistics`. The difference is that, instead of data values, it has +formatting values. Use None for a given key to indicate to the StatsPage that a +given column should not be output. Use a string with formatting +(such as '%.3f') to interpolate the value(s), or use a callable (such as +lambda v: v.isoformat()) for more advanced formatting. Any entry which is not +mentioned in the formatting dict is output unchanged. + +Monitoring +---------- + +Although the HTML output takes pains to assign unique id's to each with +statistical data, you're probably better off fetching /cpstats/data, which +outputs the whole (extrapolated) `logging.statistics` dict in JSON format. +That is probably easier to parse, and doesn't have any formatting controls, +so you get the "original" data in a consistently-serialized format. +Note: there's no treatment yet for datetime objects. Try time.time() instead +for now if you can. Nagios will probably thank you. + +Turning Collection Off +---------------------- + +It is recommended each namespace have an "Enabled" item which, if False, +stops collection (but not reporting) of statistical data. Applications +SHOULD provide controls to pause and resume collection by setting these +entries to False or True, if present. + + +Usage +===== + +To collect statistics on CherryPy applications:: + + from cherrypy.lib import cpstats + appconfig['/']['tools.cpstats.on'] = True + +To collect statistics on your own code:: + + import logging + # Initialize the repository + if not hasattr(logging, 'statistics'): logging.statistics = {} + # Initialize my namespace + mystats = logging.statistics.setdefault('My Stuff', {}) + # Initialize my namespace's scalars and collections + mystats.update({ + 'Enabled': True, + 'Start Time': time.time(), + 'Important Events': 0, + 'Events/Second': lambda s: ( + (s['Important Events'] / (time.time() - s['Start Time']))), + }) + ... + for event in events: + ... + # Collect stats + if mystats.get('Enabled', False): + mystats['Important Events'] += 1 + +To report statistics:: + + root.cpstats = cpstats.StatsPage() + +To format statistics reports:: + + See 'Reporting', above. + +""" + +import logging +import os +import sys +import threading +import time + +import cherrypy +from cherrypy._json import json + +# ------------------------------- Statistics -------------------------------- # + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +def extrapolate_statistics(scope): + """Return an extrapolated copy of the given scope.""" + c = {} + for k, v in scope.copy().items(): + if isinstance(v, dict): + v = extrapolate_statistics(v) + elif isinstance(v, (list, tuple)): + v = [extrapolate_statistics(record) for record in v] + elif hasattr(v, '__call__'): + v = v(scope) + c[k] = v + return c + + +# -------------------- CherryPy Applications Statistics --------------------- # + +appstats = logging.statistics.setdefault('CherryPy Applications', {}) +appstats.update({ + 'Enabled': True, + 'Bytes Read/Request': lambda s: ( + s['Total Requests'] and + (s['Total Bytes Read'] / float(s['Total Requests'])) or + 0.0 + ), + 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), + 'Bytes Written/Request': lambda s: ( + s['Total Requests'] and + (s['Total Bytes Written'] / float(s['Total Requests'])) or + 0.0 + ), + 'Bytes Written/Second': lambda s: ( + s['Total Bytes Written'] / s['Uptime'](s) + ), + 'Current Time': lambda s: time.time(), + 'Current Requests': 0, + 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), + 'Server Version': cherrypy.__version__, + 'Start Time': time.time(), + 'Total Bytes Read': 0, + 'Total Bytes Written': 0, + 'Total Requests': 0, + 'Total Time': 0, + 'Uptime': lambda s: time.time() - s['Start Time'], + 'Requests': {}, +}) + + +def proc_time(s): + return time.time() - s['Start Time'] + + +class ByteCountWrapper(object): + + """Wraps a file-like object, counting the number of bytes read.""" + + def __init__(self, rfile): + self.rfile = rfile + self.bytes_read = 0 + + def read(self, size=-1): + data = self.rfile.read(size) + self.bytes_read += len(data) + return data + + def readline(self, size=-1): + data = self.rfile.readline(size) + self.bytes_read += len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + return data + + +def average_uriset_time(s): + return s['Count'] and (s['Sum'] / s['Count']) or 0 + + +def _get_threading_ident(): + if sys.version_info >= (3, 3): + return threading.get_ident() + return threading._get_ident() + + +class StatsTool(cherrypy.Tool): + + """Record various information about the current request.""" + + def __init__(self): + cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + if appstats.get('Enabled', False): + cherrypy.Tool._setup(self) + self.record_start() + + def record_start(self): + """Record the beginning of a request.""" + request = cherrypy.serving.request + if not hasattr(request.rfile, 'bytes_read'): + request.rfile = ByteCountWrapper(request.rfile) + request.body.fp = request.rfile + + r = request.remote + + appstats['Current Requests'] += 1 + appstats['Total Requests'] += 1 + appstats['Requests'][_get_threading_ident()] = { + 'Bytes Read': None, + 'Bytes Written': None, + # Use a lambda so the ip gets updated by tools.proxy later + 'Client': lambda s: '%s:%s' % (r.ip, r.port), + 'End Time': None, + 'Processing Time': proc_time, + 'Request-Line': request.request_line, + 'Response Status': None, + 'Start Time': time.time(), + } + + def record_stop( + self, uriset=None, slow_queries=1.0, slow_queries_count=100, + debug=False, **kwargs): + """Record the end of a request.""" + resp = cherrypy.serving.response + w = appstats['Requests'][_get_threading_ident()] + + r = cherrypy.request.rfile.bytes_read + w['Bytes Read'] = r + appstats['Total Bytes Read'] += r + + if resp.stream: + w['Bytes Written'] = 'chunked' + else: + cl = int(resp.headers.get('Content-Length', 0)) + w['Bytes Written'] = cl + appstats['Total Bytes Written'] += cl + + w['Response Status'] = \ + getattr(resp, 'output_status', resp.status).decode() + + w['End Time'] = time.time() + p = w['End Time'] - w['Start Time'] + w['Processing Time'] = p + appstats['Total Time'] += p + + appstats['Current Requests'] -= 1 + + if debug: + cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') + + if uriset: + rs = appstats.setdefault('URI Set Tracking', {}) + r = rs.setdefault(uriset, { + 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, + 'Avg': average_uriset_time}) + if r['Min'] is None or p < r['Min']: + r['Min'] = p + if r['Max'] is None or p > r['Max']: + r['Max'] = p + r['Count'] += 1 + r['Sum'] += p + + if slow_queries and p > slow_queries: + sq = appstats.setdefault('Slow Queries', []) + sq.append(w.copy()) + if len(sq) > slow_queries_count: + sq.pop(0) + + +cherrypy.tools.cpstats = StatsTool() + + +# ---------------------- CherryPy Statistics Reporting ---------------------- # + +thisdir = os.path.abspath(os.path.dirname(__file__)) + +missing = object() + + +def locale_date(v): + return time.strftime('%c', time.gmtime(v)) + + +def iso_format(v): + return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) + + +def pause_resume(ns): + def _pause_resume(enabled): + pause_disabled = '' + resume_disabled = '' + if enabled: + resume_disabled = 'disabled="disabled" ' + else: + pause_disabled = 'disabled="disabled" ' + return """ +
+ + +
+
+ + +
+ """ % (ns, pause_disabled, ns, resume_disabled) + return _pause_resume + + +class StatsPage(object): + + formatting = { + 'CherryPy Applications': { + 'Enabled': pause_resume('CherryPy Applications'), + 'Bytes Read/Request': '%.3f', + 'Bytes Read/Second': '%.3f', + 'Bytes Written/Request': '%.3f', + 'Bytes Written/Second': '%.3f', + 'Current Time': iso_format, + 'Requests/Second': '%.3f', + 'Start Time': iso_format, + 'Total Time': '%.3f', + 'Uptime': '%.3f', + 'Slow Queries': { + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': iso_format, + }, + 'URI Set Tracking': { + 'Avg': '%.3f', + 'Max': '%.3f', + 'Min': '%.3f', + 'Sum': '%.3f', + }, + 'Requests': { + 'Bytes Read': '%s', + 'Bytes Written': '%s', + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': None, + }, + }, + 'CherryPy WSGIServer': { + 'Enabled': pause_resume('CherryPy WSGIServer'), + 'Connections/second': '%.3f', + 'Start time': iso_format, + }, + } + + @cherrypy.expose + def index(self): + # Transform the raw data into pretty output for HTML + yield """ + + + Statistics + + + +""" + for title, scalars, collections in self.get_namespaces(): + yield """ +

%s

+ + + +""" % title + for i, (key, value) in enumerate(scalars): + colnum = i % 3 + if colnum == 0: + yield """ + """ + yield ( + """ + """ % + vars() + ) + if colnum == 2: + yield """ + """ + + if colnum == 0: + yield """ + + + """ + elif colnum == 1: + yield """ + + """ + yield """ + +
%(key)s%(value)s
""" + + for subtitle, headers, subrows in collections: + yield """ +

%s

+ + + """ % subtitle + for key in headers: + yield """ + """ % key + yield """ + + + """ + for subrow in subrows: + yield """ + """ + for value in subrow: + yield """ + """ % value + yield """ + """ + yield """ + +
%s
%s
""" + yield """ + + +""" + + def get_namespaces(self): + """Yield (title, scalars, collections) for each namespace.""" + s = extrapolate_statistics(logging.statistics) + for title, ns in sorted(s.items()): + scalars = [] + collections = [] + ns_fmt = self.formatting.get(title, {}) + for k, v in sorted(ns.items()): + fmt = ns_fmt.get(k, {}) + if isinstance(v, dict): + headers, subrows = self.get_dict_collection(v, fmt) + collections.append((k, ['ID'] + headers, subrows)) + elif isinstance(v, (list, tuple)): + headers, subrows = self.get_list_collection(v, fmt) + collections.append((k, headers, subrows)) + else: + format = ns_fmt.get(k, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v = format(v) + elif format is not missing: + v = format % v + scalars.append((k, v)) + yield title, scalars, collections + + def get_dict_collection(self, v, formatting): + """Return ([headers], [rows]) for the given collection.""" + # E.g., the 'Requests' dict. + headers = [] + vals = v.values() + for record in vals: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for k2, record in sorted(v.items()): + subrow = [k2] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + def get_list_collection(self, v, formatting): + """Return ([headers], [subrows]) for the given collection.""" + # E.g., the 'Slow Queries' list. + headers = [] + for record in v: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for record in v: + subrow = [] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + if json is not None: + @cherrypy.expose + def data(self): + s = extrapolate_statistics(logging.statistics) + cherrypy.response.headers['Content-Type'] = 'application/json' + return json.dumps(s, sort_keys=True, indent=4).encode('utf-8') + + @cherrypy.expose + def pause(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = False + raise cherrypy.HTTPRedirect('./') + pause.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + + @cherrypy.expose + def resume(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = True + raise cherrypy.HTTPRedirect('./') + resume.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cptools.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cptools.py new file mode 100644 index 000000000..13b4c5679 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/cptools.py @@ -0,0 +1,639 @@ +"""Functions for builtin CherryPy tools.""" + +import logging +import re +from hashlib import md5 +import urllib.parse + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import httputil as _httputil +from cherrypy.lib import is_iterator + + +# Conditional HTTP request support # + +def validate_etags(autotags=False, debug=False): + """Validate the current ETag against If-Match, If-None-Match headers. + + If autotags is True, an ETag response-header value will be provided + from an MD5 hash of the response body (unless some other code has + already provided an ETag header). If False (the default), the ETag + will not be automatic. + + WARNING: the autotags feature is not designed for URL's which allow + methods other than GET. For example, if a POST to the same URL returns + no content, the automatic ETag will be incorrect, breaking a fundamental + use for entity tags in a possibly destructive fashion. Likewise, if you + raise 304 Not Modified, the response body will be empty, the ETag hash + will be incorrect, and your application will break. + See :rfc:`2616` Section 14.24. + """ + response = cherrypy.serving.response + + # Guard against being run twice. + if hasattr(response, 'ETag'): + return + + status, reason, msg = _httputil.valid_status(response.status) + + etag = response.headers.get('ETag') + + # Automatic ETag generation. See warning in docstring. + if etag: + if debug: + cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') + elif not autotags: + if debug: + cherrypy.log('Autotags off', 'TOOLS.ETAGS') + elif status != 200: + if debug: + cherrypy.log('Status not 200', 'TOOLS.ETAGS') + else: + etag = response.collapse_body() + etag = '"%s"' % md5(etag).hexdigest() + if debug: + cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') + response.headers['ETag'] = etag + + response.ETag = etag + + # "If the request would, without the If-Match header field, result in + # anything other than a 2xx or 412 status, then the If-Match header + # MUST be ignored." + if debug: + cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') + if status >= 200 and status <= 299: + request = cherrypy.serving.request + + conditions = request.headers.elements('If-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions and not (conditions == ['*'] or etag in conditions): + raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did ' + 'not match %r' % (etag, conditions)) + + conditions = request.headers.elements('If-None-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-None-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions == ['*'] or etag in conditions: + if debug: + cherrypy.log('request.method: %s' % + request.method, 'TOOLS.ETAGS') + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r ' + 'matched %r' % (etag, conditions)) + + +def validate_since(): + """Validate the current Last-Modified against If-Modified-Since headers. + + If no code has set the Last-Modified response header, then no validation + will be performed. + """ + response = cherrypy.serving.response + lastmod = response.headers.get('Last-Modified') + if lastmod: + status, reason, msg = _httputil.valid_status(response.status) + + request = cherrypy.serving.request + + since = request.headers.get('If-Unmodified-Since') + if since and since != lastmod: + if (status >= 200 and status <= 299) or status == 412: + raise cherrypy.HTTPError(412) + + since = request.headers.get('If-Modified-Since') + if since and since == lastmod: + if (status >= 200 and status <= 299) or status == 304: + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412) + + +# Tool code # + +def allow(methods=None, debug=False): + """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). + + The given methods are case-insensitive, and may be in any order. + If only one method is allowed, you may supply a single string; + if more than one, supply a list of strings. + + Regardless of whether the current method is allowed or not, this + also emits an 'Allow' response header, containing the given methods. + """ + if not isinstance(methods, (tuple, list)): + methods = [methods] + methods = [m.upper() for m in methods if m] + if not methods: + methods = ['GET', 'HEAD'] + elif 'GET' in methods and 'HEAD' not in methods: + methods.append('HEAD') + + cherrypy.response.headers['Allow'] = ', '.join(methods) + if cherrypy.request.method not in methods: + if debug: + cherrypy.log('request.method %r not in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + raise cherrypy.HTTPError(405) + else: + if debug: + cherrypy.log('request.method %r in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + + +def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', + scheme='X-Forwarded-Proto', debug=False): + """Change the base URL (scheme://host[:port][/path]). + + For running a CP server behind Apache, lighttpd, or other HTTP server. + + For Apache and lighttpd, you should leave the 'local' argument at the + default value of 'X-Forwarded-Host'. For Squid, you probably want to set + tools.proxy.local = 'Origin'. + + If you want the new request.base to include path info (not just the host), + you must explicitly set base to the full base path, and ALSO set 'local' + to '', so that the X-Forwarded-Host request header (which never includes + path info) does not override it. Regardless, the value for 'base' MUST + NOT end in a slash. + + cherrypy.request.remote.ip (the IP address of the client) will be + rewritten if the header specified by the 'remote' arg is valid. + By default, 'remote' is set to 'X-Forwarded-For'. If you do not + want to rewrite remote.ip, set the 'remote' arg to an empty string. + """ + + request = cherrypy.serving.request + + if scheme: + s = request.headers.get(scheme, None) + if debug: + cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') + if s == 'on' and 'ssl' in scheme.lower(): + # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header + scheme = 'https' + else: + # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' + scheme = s + if not scheme: + scheme = request.base[:request.base.find('://')] + + if local: + lbase = request.headers.get(local, None) + if debug: + cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') + if lbase is not None: + base = lbase.split(',')[0] + if not base: + default = urllib.parse.urlparse(request.base).netloc + base = request.headers.get('Host', default) + + if base.find('://') == -1: + # add http:// or https:// if needed + base = scheme + '://' + base + + request.base = base + + if remote: + xff = request.headers.get(remote) + if debug: + cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') + if xff: + if remote == 'X-Forwarded-For': + # Grab the first IP in a comma-separated list. Ref #1268. + xff = next(ip.strip() for ip in xff.split(',')) + request.remote.ip = xff + + +def ignore_headers(headers=('Range',), debug=False): + """Delete request headers whose field names are included in 'headers'. + + This is a useful tool for working behind certain HTTP servers; + for example, Apache duplicates the work that CP does for 'Range' + headers, and will doubly-truncate the response. + """ + request = cherrypy.serving.request + for name in headers: + if name in request.headers: + if debug: + cherrypy.log('Ignoring request header %r' % name, + 'TOOLS.IGNORE_HEADERS') + del request.headers[name] + + +def response_headers(headers=None, debug=False): + """Set headers on the response.""" + if debug: + cherrypy.log('Setting response headers: %s' % repr(headers), + 'TOOLS.RESPONSE_HEADERS') + for name, value in (headers or []): + cherrypy.serving.response.headers[name] = value + + +response_headers.failsafe = True + + +def referer(pattern, accept=True, accept_missing=False, error=403, + message='Forbidden Referer header.', debug=False): + """Raise HTTPError if Referer header does/does not match the given pattern. + + pattern + A regular expression pattern to test against the Referer. + + accept + If True, the Referer must match the pattern; if False, + the Referer must NOT match the pattern. + + accept_missing + If True, permit requests with no Referer header. + + error + The HTTP error code to return to the client on failure. + + message + A string to include in the response body on failure. + + """ + try: + ref = cherrypy.serving.request.headers['Referer'] + match = bool(re.match(pattern, ref)) + if debug: + cherrypy.log('Referer %r matches %r' % (ref, pattern), + 'TOOLS.REFERER') + if accept == match: + return + except KeyError: + if debug: + cherrypy.log('No Referer header', 'TOOLS.REFERER') + if accept_missing: + return + + raise cherrypy.HTTPError(error, message) + + +class SessionAuth(object): + + """Assert that the user is logged in.""" + + session_key = 'username' + debug = False + + def check_username_and_password(self, username, password): + pass + + def anonymous(self): + """Provide a temporary user name for anonymous users.""" + pass + + def on_login(self, username): + pass + + def on_logout(self, username): + pass + + def on_check(self, username): + pass + + def login_screen(self, from_page='..', username='', error_msg='', + **kwargs): + return (str(""" +Message: %(error_msg)s +
+ Login: +
+ Password: +
+ +
+ +
+""") % vars()).encode('utf-8') + + def do_login(self, username, password, from_page='..', **kwargs): + """Login. May raise redirect, or return True if request handled.""" + response = cherrypy.serving.response + error_msg = self.check_username_and_password(username, password) + if error_msg: + body = self.login_screen(from_page, username, error_msg) + response.body = body + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + else: + cherrypy.serving.request.login = username + cherrypy.session[self.session_key] = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or '/') + + def do_logout(self, from_page='..', **kwargs): + """Logout. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + username = sess.get(self.session_key) + sess[self.session_key] = None + if username: + cherrypy.serving.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page) + + def do_check(self): + """Assert username. Raise redirect, or return True if request handled. + """ + sess = cherrypy.session + request = cherrypy.serving.request + response = cherrypy.serving.response + + username = sess.get(self.session_key) + if not username: + sess[self.session_key] = username = self.anonymous() + self._debug_message('No session[username], trying anonymous') + if not username: + url = cherrypy.url(qs=request.query_string) + self._debug_message( + 'No username, routing to login_screen with from_page %(url)r', + locals(), + ) + response.body = self.login_screen(url) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + self._debug_message('Setting request.login to %(username)r', locals()) + request.login = username + self.on_check(username) + + def _debug_message(self, template, context={}): + if not self.debug: + return + cherrypy.log(template % context, 'TOOLS.SESSAUTH') + + def run(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + path = request.path_info + if path.endswith('login_screen'): + self._debug_message('routing %(path)r to login_screen', locals()) + response.body = self.login_screen() + return True + elif path.endswith('do_login'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + self._debug_message('do_login requires POST') + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_login', locals()) + return self.do_login(**request.params) + elif path.endswith('do_logout'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_logout', locals()) + return self.do_logout(**request.params) + else: + self._debug_message('No special path, running do_check') + return self.do_check() + + +def session_auth(**kwargs): + """Session authentication hook. + + Any attribute of the SessionAuth class may be overridden + via a keyword arg to this function: + + """ + '\n '.join( + '{!s}: {!s}'.format(k, type(getattr(SessionAuth, k)).__name__) + for k in dir(SessionAuth) + if not k.startswith('__') + ) + sa = SessionAuth() + for k, v in kwargs.items(): + setattr(sa, k, v) + return sa.run() + + +def log_traceback(severity=logging.ERROR, debug=False): + """Write the last error's traceback to the cherrypy error log.""" + cherrypy.log('', 'HTTP', severity=severity, traceback=True) + + +def log_request_headers(debug=False): + """Write request headers to the cherrypy error log.""" + h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list] + cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP') + + +def log_hooks(debug=False): + """Write request.hooks to the cherrypy error log.""" + request = cherrypy.serving.request + + msg = [] + # Sort by the standard points if possible. + from cherrypy import _cprequest + points = _cprequest.hookpoints + for k in request.hooks.keys(): + if k not in points: + points.append(k) + + for k in points: + msg.append(' %s:' % k) + v = request.hooks.get(k, []) + v.sort() + for h in v: + msg.append(' %r' % h) + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + + ':\n' + '\n'.join(msg), 'HTTP') + + +def redirect(url='', internal=True, debug=False): + """Raise InternalRedirect or HTTPRedirect to the given url.""" + if debug: + cherrypy.log('Redirecting %sto: %s' % + ({True: 'internal ', False: ''}[internal], url), + 'TOOLS.REDIRECT') + if internal: + raise cherrypy.InternalRedirect(url) + else: + raise cherrypy.HTTPRedirect(url) + + +def trailing_slash(missing=True, extra=False, status=None, debug=False): + """Redirect if path_info has (missing|extra) trailing slash.""" + request = cherrypy.serving.request + pi = request.path_info + + if debug: + cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % + (request.is_index, missing, extra, pi), + 'TOOLS.TRAILING_SLASH') + if request.is_index is True: + if missing: + if not pi.endswith('/'): + new_url = cherrypy.url(pi + '/', request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + elif request.is_index is False: + if extra: + # If pi == '/', don't redirect to ''! + if pi.endswith('/') and pi != '/': + new_url = cherrypy.url(pi[:-1], request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + + +def flatten(debug=False): + """Wrap response.body in a generator that recursively iterates over body. + + This allows cherrypy.response.body to consist of 'nested generators'; + that is, a set of generators that yield generators. + """ + def flattener(input): + numchunks = 0 + for x in input: + if not is_iterator(x): + numchunks += 1 + yield x + else: + for y in flattener(x): + numchunks += 1 + yield y + if debug: + cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') + response = cherrypy.serving.response + response.body = flattener(response.body) + + +def accept(media=None, debug=False): + """Return the client's preferred media-type (from the given Content-Types). + + If 'media' is None (the default), no test will be performed. + + If 'media' is provided, it should be the Content-Type value (as a string) + or values (as a list or tuple of strings) which the current resource + can emit. The client's acceptable media ranges (as declared in the + Accept request header) will be matched in order to these Content-Type + values; the first such string is returned. That is, the return value + will always be one of the strings provided in the 'media' arg (or None + if 'media' is None). + + If no match is found, then HTTPError 406 (Not Acceptable) is raised. + Note that most web browsers send */* as a (low-quality) acceptable + media range, which should match any Content-Type. In addition, "...if + no Accept header field is present, then it is assumed that the client + accepts all media types." + + Matching types are checked in order of client preference first, + and then in the order of the given 'media' values. + + Note that this function does not honor accept-params (other than "q"). + """ + if not media: + return + if isinstance(media, text_or_bytes): + media = [media] + request = cherrypy.serving.request + + # Parse the Accept request header, and try to match one + # of the requested media-ranges (in order of preference). + ranges = request.headers.elements('Accept') + if not ranges: + # Any media type is acceptable. + if debug: + cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') + return media[0] + else: + # Note that 'ranges' is sorted in order of preference + for element in ranges: + if element.qvalue > 0: + if element.value == '*/*': + # Matches any type or subtype + if debug: + cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') + return media[0] + elif element.value.endswith('/*'): + # Matches any subtype + mtype = element.value[:-1] # Keep the slash + for m in media: + if m.startswith(mtype): + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return m + else: + # Matches exact value + if element.value in media: + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return element.value + + # No suitable media-range found. + ah = request.headers.get('Accept') + if ah is None: + msg = 'Your client did not send an Accept header.' + else: + msg = 'Your client sent this Accept header: %s.' % ah + msg += (' But this resource only emits these media types: %s.' % + ', '.join(media)) + raise cherrypy.HTTPError(406, msg) + + +class MonitoredHeaderMap(_httputil.HeaderMap): + + def transform_key(self, key): + self.accessed_headers.add(key) + return super(MonitoredHeaderMap, self).transform_key(key) + + def __init__(self): + self.accessed_headers = set() + super(MonitoredHeaderMap, self).__init__() + + +def autovary(ignore=None, debug=False): + """Auto-populate the Vary response header based on request.header access. + """ + request = cherrypy.serving.request + + req_h = request.headers + request.headers = MonitoredHeaderMap() + request.headers.update(req_h) + if ignore is None: + ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) + + def set_response_header(): + resp_h = cherrypy.serving.response.headers + v = set([e.value for e in resp_h.elements('Vary')]) + if debug: + cherrypy.log( + 'Accessed headers: %s' % request.headers.accessed_headers, + 'TOOLS.AUTOVARY') + v = v.union(request.headers.accessed_headers) + v = v.difference(ignore) + v = list(v) + v.sort() + resp_h['Vary'] = ', '.join(v) + request.hooks.attach('before_finalize', set_response_header, 95) + + +def convert_params(exception=ValueError, error=400): + """Convert request params based on function annotations. + + This function also processes errors that are subclasses of ``exception``. + + :param BaseException exception: Exception class to catch. + :type exception: BaseException + + :param error: The HTTP status code to return to the client on failure. + :type error: int + """ + request = cherrypy.serving.request + types = request.handler.callable.__annotations__ + with cherrypy.HTTPError.handle(exception, error): + for key in set(types).intersection(request.params): + request.params[key] = types[key](request.params[key]) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/encoding.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/encoding.py new file mode 100644 index 000000000..c2c478a50 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/encoding.py @@ -0,0 +1,454 @@ +import struct +import time +import io + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import file_generator +from cherrypy.lib import is_closable_iterator +from cherrypy.lib import set_vary_header + + +_COMPRESSION_LEVEL_FAST = 1 +_COMPRESSION_LEVEL_BEST = 9 + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is + given in the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request + entity is *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class UTF8StreamEncoder: + def __init__(self, iterator): + self._iterator = iterator + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + res = next(self._iterator) + if isinstance(res, str): + res = res.encode('utf-8') + return res + + def close(self): + if is_closable_iterator(self._iterator): + self._iterator.close() + + def __getattr__(self, attr): + if attr.startswith('__'): + raise AttributeError(self, attr) + return getattr(self._iterator, attr) + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = 'Response body could not be encoded with %r.' + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, str): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + body = [] + for chunk in self.body: + if isinstance(chunk, str): + try: + chunk = chunk.encode(encoding, self.errors) + except (LookupError, UnicodeError): + return False + body.append(chunk) + self.body = body + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % + response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> t = u"\u7007\u3040" + # >>> len(t) + # 2 + # >>> len(t.encode("UTF-8")) + # 6 + # >>> len(t.encode("utf7")) + # 8 + del response.headers['Content-Length'] + + # Parse the Accept-Charset request header, and try to provide one + # of the requested charsets (in order of user preference). + encs = request.headers.elements('Accept-Charset') + charsets = [enc.value.lower() for enc in encs] + if self.debug: + cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') + + if self.encoding is not None: + # If specified, force this encoding to be used, or fail. + encoding = self.encoding.lower() + if self.debug: + cherrypy.log('Specified encoding %r' % + encoding, 'TOOLS.ENCODE') + if (not charsets) or '*' in charsets or encoding in charsets: + if self.debug: + cherrypy.log('Attempting encoding %r' % + encoding, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + else: + if not encs: + if self.debug: + cherrypy.log('Attempting default encoding %r' % + self.default_encoding, 'TOOLS.ENCODE') + # Any character-set is acceptable. + if encoder(self.default_encoding): + return self.default_encoding + else: + raise cherrypy.HTTPError(500, self.failmsg % + self.default_encoding) + else: + for element in encs: + if element.qvalue > 0: + if element.value == '*': + # Matches any charset. Try our default. + if self.debug: + cherrypy.log('Attempting default encoding due ' + 'to %r' % element, 'TOOLS.ENCODE') + if encoder(self.default_encoding): + return self.default_encoding + else: + encoding = element.value + if self.debug: + cherrypy.log('Attempting encoding %s (qvalue >' + '0)' % element, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + + if '*' not in charsets: + # If no "*" is present in an Accept-Charset field, then all + # character sets not explicitly mentioned get a quality + # value of 0, except for ISO-8859-1, which gets a quality + # value of 1 if not explicitly mentioned. + iso = 'iso-8859-1' + if iso not in charsets: + if self.debug: + cherrypy.log('Attempting ISO-8859-1 encoding', + 'TOOLS.ENCODE') + if encoder(iso): + return iso + + # No suitable encoding found. + ac = request.headers.get('Accept-Charset') + if ac is None: + msg = 'Your client did not send an Accept-Charset header.' + else: + msg = 'Your client sent this Accept-Charset header: %s.' % ac + _charsets = ', '.join(sorted(self.attempted_charsets)) + msg += ' We tried these charsets: %s.' % (_charsets,) + raise cherrypy.HTTPError(406, msg) + + def __call__(self, *args, **kwargs): + response = cherrypy.serving.response + self.body = self.oldhandler(*args, **kwargs) + + self.body = prepare_iter(self.body) + + ct = response.headers.elements('Content-Type') + if self.debug: + cherrypy.log('Content-Type: %r' % [str(h) + for h in ct], 'TOOLS.ENCODE') + if ct and self.add_charset: + ct = ct[0] + if self.text_only: + if ct.value.lower().startswith('text/'): + if self.debug: + cherrypy.log( + 'Content-Type %s starts with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = True + else: + if self.debug: + cherrypy.log('Not finding because Content-Type %s ' + 'does not start with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = False + else: + if self.debug: + cherrypy.log('Finding because not text_only', + 'TOOLS.ENCODE') + do_find = True + + if do_find: + # Set "charset=..." param on response Content-Type header + ct.params['charset'] = self.find_acceptable_charset() + if self.debug: + cherrypy.log('Setting Content-Type %s' % ct, + 'TOOLS.ENCODE') + response.headers['Content-Type'] = str(ct) + + return self.body + + +def prepare_iter(value): + """ + Ensure response body is iterable and resolves to False when empty. + """ + if isinstance(value, text_or_bytes): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if value: + value = [value] + else: + # [''] doesn't evaluate to False, so replace it with []. + value = [] + # Don't use isinstance here; io.IOBase which has an ABC takes + # 1000 times as long as, say, isinstance(value, str) + elif hasattr(value, 'read'): + value = file_generator(value) + elif value is None: + value = [] + return value + + +# GZIP + + +def compress(body, compress_level): + """Compress 'body' at the given compress_level.""" + import zlib + + # See https://tools.ietf.org/html/rfc1952 + yield b'\x1f\x8b' # ID1 and ID2: gzip marker + yield b'\x08' # CM: compression method + yield b'\x00' # FLG: none set + # MTIME: 4 bytes + yield struct.pack(' 0 is present + * The 'identity' value is given with a qvalue > 0. + + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + set_vary_header(response, 'Accept-Encoding') + + if not response.body: + # Response body is empty (might be a 304 for instance) + if debug: + cherrypy.log('No response body', context='TOOLS.GZIP') + return + + # If returning cached content (which should already have been gzipped), + # don't re-zip. + if getattr(request, 'cached', False): + if debug: + cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') + return + + acceptable = request.headers.elements('Accept-Encoding') + if not acceptable: + # If no Accept-Encoding field is present in a request, + # the server MAY assume that the client will accept any + # content coding. In this case, if "identity" is one of + # the available content-codings, then the server SHOULD use + # the "identity" content-coding, unless it has additional + # information that a different content-coding is meaningful + # to the client. + if debug: + cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') + return + + ct = response.headers.get('Content-Type', '').split(';')[0] + for coding in acceptable: + if coding.value == 'identity' and coding.qvalue != 0: + if debug: + cherrypy.log('Non-zero identity qvalue: %s' % coding, + context='TOOLS.GZIP') + return + if coding.value in ('gzip', 'x-gzip'): + if coding.qvalue == 0: + if debug: + cherrypy.log('Zero gzip qvalue: %s' % coding, + context='TOOLS.GZIP') + return + + if ct not in mime_types: + # If the list of provided mime-types contains tokens + # such as 'text/*' or 'application/*+xml', + # we go through them and find the most appropriate one + # based on the given content-type. + # The pattern matching is only caring about the most + # common cases, as stated above, and doesn't support + # for extra parameters. + found = False + if '/' in ct: + ct_media_type, ct_sub_type = ct.split('/') + for mime_type in mime_types: + if '/' in mime_type: + media_type, sub_type = mime_type.split('/') + if ct_media_type == media_type: + if sub_type == '*': + found = True + break + elif '+' in sub_type and '+' in ct_sub_type: + ct_left, ct_right = ct_sub_type.split('+') + left, right = sub_type.split('+') + if left == '*' and ct_right == right: + found = True + break + + if not found: + if debug: + cherrypy.log('Content-Type %s not in mime_types %r' % + (ct, mime_types), context='TOOLS.GZIP') + return + + if debug: + cherrypy.log('Gzipping', context='TOOLS.GZIP') + # Return a generator that compresses the page + response.headers['Content-Encoding'] = 'gzip' + response.body = compress(response.body, compress_level) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + return + + if debug: + cherrypy.log('No acceptable encoding found.', context='GZIP') + cherrypy.HTTPError(406, 'identity, gzip').set_response() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/gctools.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/gctools.py new file mode 100644 index 000000000..26746d78b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/gctools.py @@ -0,0 +1,218 @@ +import gc +import inspect +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > self.maxparents: + return [('[%s referrers]' % len(refs), [])] + + try: + ascendcode = self.ascend.__code__ + except AttributeError: + ascendcode = self.ascend.im_func.func_code + for parent in refs: + if inspect.isframe(parent) and parent.f_code is ascendcode: + continue + if parent in self.ignore: + continue + if depth <= self.maxdepth: + parents.append((parent, self.ascend(parent, depth))) + else: + parents.append((parent, [])) + + return parents + + def peek(self, s): + """Return s, restricted to a sane length.""" + if len(s) > (self.peek_length + 3): + half = self.peek_length // 2 + return s[:half] + '...' + s[-half:] + else: + return s + + def _format(self, obj, descend=True): + """Return a string representation of a single object.""" + if inspect.isframe(obj): + filename, lineno, func, context, index = inspect.getframeinfo(obj) + return "" % func + + if not descend: + return self.peek(repr(obj)) + + if isinstance(obj, dict): + return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False), + self._format(v, descend=False)) + for k, v in obj.items()]) + '}' + elif isinstance(obj, list): + return '[' + ', '.join([self._format(item, descend=False) + for item in obj]) + ']' + elif isinstance(obj, tuple): + return '(' + ', '.join([self._format(item, descend=False) + for item in obj]) + ')' + + r = self.peek(repr(obj)) + if isinstance(obj, (str, int, float)): + return r + return '%s: %s' % (type(obj), r) + + def format(self, tree): + """Return a list of string reprs from a nested list of referrers.""" + output = [] + + def ascend(branch, depth=1): + for parent, grandparents in branch: + output.append((' ' * depth) + self._format(parent)) + if grandparents: + ascend(grandparents, depth + 1) + ascend(tree) + return output + + +def get_instances(cls): + return [x for x in gc.get_objects() if isinstance(x, cls)] + + +class RequestCounter(SimplePlugin): + + def start(self): + self.count = 0 + + def before_request(self): + self.count += 1 + + def after_request(self): + self.count -= 1 + + +request_counter = RequestCounter(cherrypy.engine) +request_counter.subscribe() + + +def get_context(obj): + if isinstance(obj, _cprequest.Request): + return 'path=%s;stage=%s' % (obj.path_info, obj.stage) + elif isinstance(obj, _cprequest.Response): + return 'status=%s' % obj.status + elif isinstance(obj, _cpwsgi.AppResponse): + return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '') + elif hasattr(obj, 'tb_lineno'): + return 'tb_lineno=%s' % obj.tb_lineno + return '' + + +class GCRoot(object): + + """A CherryPy page handler for testing reference leaks.""" + + classes = [ + (_cprequest.Request, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cprequest.Response, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cpwsgi.AppResponse, 1, 1, + 'Should be 1 in this request thread only.'), + ] + + @cherrypy.expose + def index(self): + return 'Hello, world!' + + @cherrypy.expose + def stats(self): + output = ['Statistics:'] + + for trial in range(10): + if request_counter.count > 0: + break + time.sleep(0.5) + else: + output.append('\nNot all requests closed properly.') + + # gc_collect isn't perfectly synchronous, because it may + # break reference cycles that then take time to fully + # finalize. Call it thrice and hope for the best. + gc.collect() + gc.collect() + unreachable = gc.collect() + if unreachable: + if objgraph is not None: + final = objgraph.by_type('Nondestructible') + if final: + objgraph.show_backrefs(final, filename='finalizers.png') + + trash = {} + for x in gc.garbage: + trash[type(x)] = trash.get(type(x), 0) + 1 + if trash: + output.insert(0, '\n%s unreachable objects:' % unreachable) + trash = [(v, k) for k, v in trash.items()] + trash.sort() + for pair in trash: + output.append(' ' + repr(pair)) + + # Check declared classes to verify uncollected instances. + # These don't have to be part of a cycle; they can be + # any objects that have unanticipated referrers that keep + # them from being collected. + allobjs = {} + for cls, minobj, maxobj, msg in self.classes: + allobjs[cls] = get_instances(cls) + + for cls, minobj, maxobj, msg in self.classes: + objs = allobjs[cls] + lenobj = len(objs) + if lenobj < minobj or lenobj > maxobj: + if minobj == maxobj: + output.append( + '\nExpected %s %r references, got %s.' % + (minobj, cls, lenobj)) + else: + output.append( + '\nExpected %s to %s %r references, got %s.' % + (minobj, maxobj, cls, lenobj)) + + for obj in objs: + if objgraph is not None: + ig = [id(objs), id(inspect.currentframe())] + fname = 'graph_%s_%s.png' % (cls.__name__, id(obj)) + objgraph.show_backrefs( + obj, extra_ignore=ig, max_depth=4, too_many=20, + filename=fname, extra_info=get_context) + output.append('\nReferrers for %s (refcount=%s):' % + (repr(obj), sys.getrefcount(obj))) + t = ReferrerTree(ignore=[objs], maxdepth=3) + tree = t.ascend(obj) + output.extend(t.format(tree)) + + return '\n'.join(output) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/httputil.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/httputil.py new file mode 100644 index 000000000..ced310a0a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/httputil.py @@ -0,0 +1,548 @@ +"""HTTP library functions. + +This module contains functions for building an HTTP application +framework: any one, not just one whose name starts with "Ch". ;) If you +reference any modules from some popular framework inside *this* module, +FuManChu will personally hang you up by your thumbs and submit you +to a public caning. +""" + +import functools +import email.utils +import re +import builtins +from binascii import b2a_base64 +from cgi import parse_header +from email.header import decode_header +from http.server import BaseHTTPRequestHandler +from urllib.parse import unquote_plus + +import jaraco.collections + +import cherrypy +from cherrypy._cpcompat import ntob, ntou + +response_codes = BaseHTTPRequestHandler.responses.copy() + +# From https://github.com/cherrypy/cherrypy/issues/361 +response_codes[500] = ('Internal Server Error', + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.') +response_codes[503] = ('Service Unavailable', + 'The server is currently unable to handle the ' + 'request due to a temporary overloading or ' + 'maintenance of the server.') + + +HTTPDate = functools.partial(email.utils.formatdate, usegmt=True) + + +def urljoin(*atoms): + r"""Return the given path \*atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = '/'.join([x for x in atoms if x]) + while '//' in url: + url = url.replace('//', '/') + # Special-case the final url of "", and return "/" instead. + return url or '/' + + +def urljoin_bytes(*atoms): + """Return the given path `*atoms`, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = b'/'.join([x for x in atoms if x]) + while b'//' in url: + url = url.replace(b'//', b'/') + # Special-case the final url of "", and return "/" instead. + return url or b'/' + + +def protocol_from_http(protocol_str): + """Return a protocol tuple from the given 'HTTP/x.y' string.""" + return int(protocol_str[5]), int(protocol_str[7]) + + +def get_ranges(headervalue, content_length): + """Return a list of (start, stop) indices from a Range header, or None. + + Each (start, stop) tuple will be composed of two ints, which are suitable + for use in a slicing operation. That is, the header "Range: bytes=3-6", + if applied against a Python string, is requesting resource[3:7]. This + function will return the list [(3, 7)]. + + If this function returns an empty list, you should return HTTP 416. + """ + + if not headervalue: + return None + + result = [] + bytesunit, byteranges = headervalue.split('=', 1) + for brange in byteranges.split(','): + start, stop = [x.strip() for x in brange.split('-', 1)] + if start: + if not stop: + stop = content_length - 1 + start, stop = int(start), int(stop) + if start >= content_length: + # From rfc 2616 sec 14.16: + # "If the server receives a request (other than one + # including an If-Range request-header field) with an + # unsatisfiable Range request-header field (that is, + # all of whose byte-range-spec values have a first-byte-pos + # value greater than the current length of the selected + # resource), it SHOULD return a response code of 416 + # (Requested range not satisfiable)." + continue + if stop < start: + # From rfc 2616 sec 14.16: + # "If the server ignores a byte-range-spec because it + # is syntactically invalid, the server SHOULD treat + # the request as if the invalid Range header field + # did not exist. (Normally, this means return a 200 + # response containing the full entity)." + return None + result.append((start, stop + 1)) + else: + if not stop: + # See rfc quote above. + return None + # Negative subscript (last N bytes) + # + # RFC 2616 Section 14.35.1: + # If the entity is shorter than the specified suffix-length, + # the entire entity-body is used. + if int(stop) > content_length: + result.append((0, content_length)) + else: + result.append((content_length - int(stop), content_length)) + + return result + + +class HeaderElement(object): + + """An element (with parameters) from an HTTP header's element list.""" + + def __init__(self, value, params=None): + self.value = value + if params is None: + params = {} + self.params = params + + def __cmp__(self, other): + return builtins.cmp(self.value, other.value) + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + p = [';%s=%s' % (k, v) for k, v in self.params.items()] + return str('%s%s' % (self.value, ''.join(p))) + + def __bytes__(self): + return ntob(self.__str__()) + + def __unicode__(self): + return ntou(self.__str__()) + + @staticmethod + def parse(elementstr): + """Transform 'token;key=val' to ('token', {'key': 'val'}).""" + initial_value, params = parse_header(elementstr) + return initial_value, params + + @classmethod + def from_str(cls, elementstr): + """Construct an instance from a string of the form 'token;key=val'.""" + ival, params = cls.parse(elementstr) + return cls(ival, params) + + +q_separator = re.compile(r'; *q *=') + + +class AcceptElement(HeaderElement): + + """An element (with parameters) from an Accept* header's element list. + + AcceptElement objects are comparable; the more-preferred object will be + "less than" the less-preferred object. They are also therefore sortable; + if you sort a list of AcceptElement objects, they will be listed in + priority order; the most preferred value will be first. Yes, it should + have been the other way around, but it's too late to fix now. + """ + + @classmethod + def from_str(cls, elementstr): + qvalue = None + # The first "q" parameter (if any) separates the initial + # media-range parameter(s) (if any) from the accept-params. + atoms = q_separator.split(elementstr, 1) + media_range = atoms.pop(0).strip() + if atoms: + # The qvalue for an Accept header can have extensions. The other + # headers cannot, but it's easier to parse them as if they did. + qvalue = HeaderElement.from_str(atoms[0].strip()) + + media_type, params = cls.parse(media_range) + if qvalue is not None: + params['q'] = qvalue + return cls(media_type, params) + + @property + def qvalue(self): + 'The qvalue, or priority, of this value.' + val = self.params.get('q', '1') + if isinstance(val, HeaderElement): + val = val.value + try: + return float(val) + except ValueError as val_err: + """Fail client requests with invalid quality value. + + Ref: https://github.com/cherrypy/cherrypy/issues/1370 + """ + raise cherrypy.HTTPError( + 400, + 'Malformed HTTP header: `{}`'. + format(str(self)), + ) from val_err + + def __cmp__(self, other): + diff = builtins.cmp(self.qvalue, other.qvalue) + if diff == 0: + diff = builtins.cmp(str(self), str(other)) + return diff + + def __lt__(self, other): + if self.qvalue == other.qvalue: + return str(self) < str(other) + else: + return self.qvalue < other.qvalue + + +RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') + + +def header_elements(fieldname, fieldvalue): + """Return a sorted HeaderElement list from a comma-separated header string. + """ + if not fieldvalue: + return [] + + result = [] + for element in RE_HEADER_SPLIT.split(fieldvalue): + if fieldname.startswith('Accept') or fieldname == 'TE': + hv = AcceptElement.from_str(element) + else: + hv = HeaderElement.from_str(element) + result.append(hv) + + return list(reversed(sorted(result))) + + +def decode_TEXT(value): + r""" + Decode :rfc:`2047` TEXT + + >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') + True + """ + atoms = decode_header(value) + decodedvalue = '' + for atom, charset in atoms: + if charset is not None: + atom = atom.decode(charset) + decodedvalue += atom + return decodedvalue + + +def decode_TEXT_maybe(value): + """ + Decode the text but only if '=?' appears in it. + """ + return decode_TEXT(value) if '=?' in value else value + + +def valid_status(status): + """Return legal HTTP status Code, Reason-phrase and Message. + + The status arg must be an int, a str that begins with an int + or the constant from ``http.client`` stdlib module. + + If status has no reason-phrase is supplied, a default reason- + phrase will be provided. + + >>> import http.client + >>> from http.server import BaseHTTPRequestHandler + >>> valid_status(http.client.ACCEPTED) == ( + ... int(http.client.ACCEPTED), + ... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED] + True + """ + + if not status: + status = 200 + + code, reason = status, None + if isinstance(status, str): + code, _, reason = status.partition(' ') + reason = reason.strip() or None + + try: + code = int(code) + except (TypeError, ValueError): + raise ValueError('Illegal response status from server ' + '(%s is non-numeric).' % repr(code)) + + if code < 100 or code > 599: + raise ValueError('Illegal response status from server ' + '(%s is out of range).' % repr(code)) + + if code not in response_codes: + # code is unknown but not illegal + default_reason, message = '', '' + else: + default_reason, message = response_codes[code] + + if reason is None: + reason = default_reason + + return code, reason, message + + +# NOTE: the parse_qs functions that follow are modified version of those +# in the python3.0 source - we need to pass through an encoding to the unquote +# method, but the default parse_qs function doesn't allow us to. These do. + +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): + """Parse a query given as a string argument. + + Arguments: + + qs: URL-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + URL encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + Returns a dict, as G-d intended. + """ + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + d = {} + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError('bad query field: %r' % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = unquote_plus(nv[0], encoding, errors='strict') + value = unquote_plus(nv[1], encoding, errors='strict') + if name in d: + if not isinstance(d[name], list): + d[name] = [d[name]] + d[name].append(value) + else: + d[name] = value + return d + + +image_map_pattern = re.compile(r'[0-9]+,[0-9]+') + + +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): + """Build a params dictionary from a query_string. + + Duplicate key/value pairs in the provided query_string will be + returned as {'key': [val1, val2, ...]}. Single key/values will + be returned as strings: {'key': 'value'}. + """ + if image_map_pattern.match(query_string): + # Server-side image map. Map the coords to 'x' and 'y' + # (like CGI::Request does). + pm = query_string.split(',') + pm = {'x': int(pm[0]), 'y': int(pm[1])} + else: + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) + return pm + + +class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict): + + """A case-insensitive dict subclass. + + Each key is changed on entry to title case. + """ + + @staticmethod + def transform_key(key): + if key is None: + # TODO(#1830): why? + return 'None' + return key.title() + + +# TEXT = +# +# A CRLF is allowed in the definition of TEXT only as part of a header +# field continuation. It is expected that the folding LWS will be +# replaced with a single SP before interpretation of the TEXT value." +if str == bytes: + header_translate_table = ''.join([chr(i) for i in range(256)]) + header_translate_deletechars = ''.join( + [chr(i) for i in range(32)]) + chr(127) +else: + header_translate_table = None + header_translate_deletechars = bytes(range(32)) + bytes([127]) + + +class HeaderMap(CaseInsensitiveDict): + + """A dict subclass for HTTP request and response headers. + + Each key is changed on entry to str(key).title(). This allows headers + to be case-insensitive and avoid duplicates. + + Values are header values (decoded according to :rfc:`2047` if necessary). + """ + + protocol = (1, 1) + encodings = ['ISO-8859-1'] + + # Someday, when http-bis is done, this will probably get dropped + # since few servers, clients, or intermediaries do it. But until then, + # we're going to obey the spec as is. + # "Words of *TEXT MAY contain characters from character sets other than + # ISO-8859-1 only when encoded according to the rules of RFC 2047." + use_rfc_2047 = True + + def elements(self, key): + """Return a sorted list of HeaderElements for the given header.""" + return header_elements(self.transform_key(key), self.get(key)) + + def values(self, key): + """Return a sorted list of HeaderElement.value for the given header.""" + return [e.value for e in self.elements(key)] + + def output(self): + """Transform self into a list of (name, value) tuples.""" + return list(self.encode_header_items(self.items())) + + @classmethod + def encode_header_items(cls, header_items): + """ + Prepare the sequence of name, value tuples into a form suitable for + transmitting on the wire for HTTP. + """ + for k, v in header_items: + if not isinstance(v, str) and not isinstance(v, bytes): + v = str(v) + + yield tuple(map(cls.encode_header_item, (k, v))) + + @classmethod + def encode_header_item(cls, item): + if isinstance(item, str): + item = cls.encode(item) + + # See header_translate_* constants above. + # Replace only if you really know what you're doing. + return item.translate( + header_translate_table, header_translate_deletechars) + + @classmethod + def encode(cls, v): + """Return the given header name or value, encoded for HTTP output.""" + for enc in cls.encodings: + try: + return v.encode(enc) + except UnicodeEncodeError: + continue + + if cls.protocol == (1, 1) and cls.use_rfc_2047: + # Encode RFC-2047 TEXT + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). + # We do our own here instead of using the email module + # because we never want to fold lines--folding has + # been deprecated by the HTTP working group. + v = b2a_base64(v.encode('utf-8')) + return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=') + + raise ValueError('Could not encode header part %r using ' + 'any of the encodings %r.' % + (v, cls.encodings)) + + +class Host(object): + + """An internet address. + + name + Should be the client's host name. If not available (because no DNS + lookup is performed), the IP address should be used instead. + + """ + + ip = '0.0.0.0' + port = 80 + name = 'unknown.tld' + + def __init__(self, ip, port, name=None): + self.ip = ip + self.port = port + if name is None: + name = ip + self.name = name + + def __repr__(self): + return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) + + +class SanitizedHost(str): + r""" + Wraps a raw host header received from the network in + a sanitized version that elides dangerous characters. + + >>> SanitizedHost('foo\nbar') + 'foobar' + >>> SanitizedHost('foo\nbar').raw + 'foo\nbar' + + A SanitizedInstance is only returned if sanitization was performed. + + >>> isinstance(SanitizedHost('foobar'), SanitizedHost) + False + """ + dangerous = re.compile(r'[\n\r]') + + def __new__(cls, raw): + sanitized = cls._sanitize(raw) + if sanitized == raw: + return raw + instance = super().__new__(cls, sanitized) + instance.raw = raw + return instance + + @classmethod + def _sanitize(cls, raw): + return cls.dangerous.sub('', raw) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/jsontools.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/jsontools.py new file mode 100644 index 000000000..9ca75a8f3 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/jsontools.py @@ -0,0 +1,89 @@ +import cherrypy +from cherrypy import _json as json +from cherrypy._cpcompat import text_or_bytes, ntou + + +def json_processor(entity): + """Read application/json data into request.json.""" + if not entity.headers.get(ntou('Content-Length'), ntou('')): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): + cherrypy.serving.request.json = json.decode(body.decode('utf-8')) + + +def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], + force=True, debug=False, processor=json_processor): + """Add a processor to parse JSON request entities: + The default processor places the parsed data into request.json. + + Incoming request entities which match the given content_type(s) will + be deserialized from JSON to the Python equivalent, and the result + stored at cherrypy.request.json. The 'content_type' argument may + be a Content-Type string or a list of allowable Content-Type strings. + + If the 'force' argument is True (the default), then entities of other + content types will not be allowed; "415 Unsupported Media Type" is + raised instead. + + Supply your own processor to use a custom decoder, or to handle the parsed + data differently. The processor can be configured via + tools.json_in.processor or via the decorator method. + + Note that the deserializer requires the client send a Content-Length + request header, or it will raise "411 Length Required". If for any + other reason the request entity cannot be deserialized from JSON, + it will raise "400 Bad Request: Invalid JSON document". + """ + request = cherrypy.serving.request + if isinstance(content_type, text_or_bytes): + content_type = [content_type] + + if force: + if debug: + cherrypy.log('Removing body processors %s' % + repr(request.body.processors.keys()), 'TOOLS.JSON_IN') + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an entity of content type %s' % + ', '.join(content_type)) + + for ct in content_type: + if debug: + cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') + request.body.processors[ct] = processor + + +def json_handler(*args, **kwargs): + value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) + return json.encode(value) + + +def json_out(content_type='application/json', debug=False, + handler=json_handler): + """Wrap request.handler to serialize its output to JSON. Sets Content-Type. + + If the given content_type is None, the Content-Type response header + is not set. + + Provide your own handler to use a custom encoder. For example + cherrypy.config['tools.json_out.handler'] = , or + @json_out(handler=function). + """ + request = cherrypy.serving.request + # request.handler may be set to None by e.g. the caching tool + # to signal to all components that a response body has already + # been attached, in which case we don't need to wrap anything. + if request.handler is None: + return + if debug: + cherrypy.log('Replacing %s with JSON handler' % request.handler, + 'TOOLS.JSON_OUT') + request._json_inner_handler = request.handler + request.handler = handler + if content_type is not None: + if debug: + cherrypy.log('Setting Content-Type to %s' % + content_type, 'TOOLS.JSON_OUT') + cherrypy.serving.response.headers['Content-Type'] = content_type diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/locking.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/locking.py new file mode 100644 index 000000000..317fb58c9 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/locking.py @@ -0,0 +1,47 @@ +import datetime + + +class NeverExpires(object): + def expired(self): + return False + + +class Timer(object): + """ + A simple timer that will indicate when an expiration time has passed. + """ + def __init__(self, expiration): + 'Create a timer that expires at `expiration` (UTC datetime)' + self.expiration = expiration + + @classmethod + def after(cls, elapsed): + """ + Return a timer that will expire after `elapsed` passes. + """ + return cls(datetime.datetime.utcnow() + elapsed) + + def expired(self): + return datetime.datetime.utcnow() >= self.expiration + + +class LockTimeout(Exception): + 'An exception when a lock could not be acquired before a timeout period' + + +class LockChecker(object): + """ + Keep track of the time and detect if a timeout has expired + """ + def __init__(self, session_id, timeout): + self.session_id = session_id + if timeout: + self.timer = Timer.after(timeout) + else: + self.timer = NeverExpires() + + def expired(self): + if self.timer.expired(): + raise LockTimeout( + 'Timeout acquiring lock for %(session_id)s' % vars(self)) + return False diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/profiler.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/profiler.py new file mode 100644 index 000000000..7182278aa --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/profiler.py @@ -0,0 +1,223 @@ +"""Profiler tools for CherryPy. + +CherryPy users +============== + +You can profile any of your pages as follows:: + + from cherrypy.lib import profiler + + class Root: + p = profiler.Profiler("/path/to/profile/dir") + + @cherrypy.expose + def index(self): + self.p.run(self._index) + + def _index(self): + return "Hello, world!" + + cherrypy.tree.mount(Root()) + +You can also turn on profiling for all requests +using the ``make_app`` function as WSGI middleware. + +CherryPy developers +=================== + +This module can be used whenever you make changes to CherryPy, +to get a quick sanity-check on overall CP performance. Use the +``--profile`` flag when running the test suite. Then, use the ``serve()`` +function to browse the results in a web browser. If you run this +module from the command line, it will call ``serve()`` for you. + +""" + +import io +import os +import os.path +import sys +import warnings + +import cherrypy + + +try: + import profile + import pstats + + def new_func_strip_path(func_name): + """Add ``__init__`` modules' parents. + + This makes the profiler output more readable. + """ + filename, line, name = func_name + if filename.endswith('__init__.py'): + return ( + os.path.basename(filename[:-12]) + filename[-12:], + line, + name, + ) + return os.path.basename(filename), line, name + + pstats.func_strip_path = new_func_strip_path +except ImportError: + profile = None + pstats = None + + +_count = 0 + + +class Profiler(object): + + def __init__(self, path=None): + if not path: + path = os.path.join(os.path.dirname(__file__), 'profile') + self.path = path + if not os.path.exists(path): + os.makedirs(path) + + def run(self, func, *args, **params): + """Dump profile data into self.path.""" + global _count + c = _count = _count + 1 + path = os.path.join(self.path, 'cp_%04d.prof' % c) + prof = profile.Profile() + result = prof.runcall(func, *args, **params) + prof.dump_stats(path) + return result + + def statfiles(self): + """:rtype: list of available profiles. + """ + return [f for f in os.listdir(self.path) + if f.startswith('cp_') and f.endswith('.prof')] + + def stats(self, filename, sortby='cumulative'): + """:rtype stats(index): output of print_stats() for the given profile. + """ + sio = io.StringIO() + if sys.version_info >= (2, 5): + s = pstats.Stats(os.path.join(self.path, filename), stream=sio) + s.strip_dirs() + s.sort_stats(sortby) + s.print_stats() + else: + # pstats.Stats before Python 2.5 didn't take a 'stream' arg, + # but just printed to stdout. So re-route stdout. + s = pstats.Stats(os.path.join(self.path, filename)) + s.strip_dirs() + s.sort_stats(sortby) + oldout = sys.stdout + try: + sys.stdout = sio + s.print_stats() + finally: + sys.stdout = oldout + response = sio.getvalue() + sio.close() + return response + + @cherrypy.expose + def index(self): + return """ + CherryPy profile data + + + + + + """ + + @cherrypy.expose + def menu(self): + yield '

Profiling runs

' + yield '

Click on one of the runs below to see profiling data.

' + runs = self.statfiles() + runs.sort() + for i in runs: + yield "%s
" % ( + i, i) + + @cherrypy.expose + def report(self, filename): + cherrypy.response.headers['Content-Type'] = 'text/plain' + return self.stats(filename) + + +class ProfileAggregator(Profiler): + + def __init__(self, path=None): + Profiler.__init__(self, path) + global _count + self.count = _count = _count + 1 + self.profiler = profile.Profile() + + def run(self, func, *args, **params): + path = os.path.join(self.path, 'cp_%04d.prof' % self.count) + result = self.profiler.runcall(func, *args, **params) + self.profiler.dump_stats(path) + return result + + +class make_app: + + def __init__(self, nextapp, path=None, aggregate=False): + """Make a WSGI middleware app which wraps 'nextapp' with profiling. + + nextapp + the WSGI application to wrap, usually an instance of + cherrypy.Application. + + path + where to dump the profiling output. + + aggregate + if True, profile data for all HTTP requests will go in + a single file. If False (the default), each HTTP request will + dump its profile data into a separate file. + + """ + if profile is None or pstats is None: + msg = ('Your installation of Python does not have a profile ' + "module. If you're on Debian, try " + '`sudo apt-get install python-profiler`. ' + 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'for details.') + warnings.warn(msg) + + self.nextapp = nextapp + self.aggregate = aggregate + if aggregate: + self.profiler = ProfileAggregator(path) + else: + self.profiler = Profiler(path) + + def __call__(self, environ, start_response): + def gather(): + result = [] + for line in self.nextapp(environ, start_response): + result.append(line) + return result + return self.profiler.run(gather) + + +def serve(path=None, port=8080): + if profile is None or pstats is None: + msg = ('Your installation of Python does not have a profile module. ' + "If you're on Debian, try " + '`sudo apt-get install python-profiler`. ' + 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'for details.') + warnings.warn(msg) + + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': 'production', + }) + cherrypy.quickstart(Profiler(path)) + + +if __name__ == '__main__': + serve(*tuple(sys.argv[1:])) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/reprconf.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/reprconf.py new file mode 100644 index 000000000..536b94173 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/reprconf.py @@ -0,0 +1,394 @@ +"""Generic configuration system using unrepr. + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, Python's +builtin ConfigParser is used (with some extensions). + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. + +The only key that cannot exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +the Config.environments dict. + +You can define your own namespaces to be called when new config is merged +by adding a named handler to Config.namespaces. The name can be any string, +and the handler must be either a callable or a context manager. +""" + +import builtins +import configparser +import operator +import sys + +from cherrypy._cpcompat import text_or_bytes + + +class NamespaceSet(dict): + + """A dict of config namespace names and handlers. + + Each config entry should begin with a namespace name; the corresponding + namespace handler will be called once for each config entry in that + namespace, and will be passed two arguments: the config key (with the + namespace removed) and the config value. + + Namespace handlers may be any Python callable; they may also be + context managers, in which case their __enter__ + method should return a callable to be used as the handler. + See cherrypy.tools (the Toolbox class) for an example. + """ + + def __call__(self, config): + """Iterate through config and pass it to each namespace handler. + + config + A flat dict, where keys use dots to separate + namespaces, and values are arbitrary. + + The first name in each config key is used to look up the corresponding + namespace handler. For example, a config entry of {'tools.gzip.on': v} + will call the 'tools' namespace handler with the args: ('gzip.on', v) + """ + # Separate the given config into namespaces + ns_confs = {} + for k in config: + if '.' in k: + ns, name = k.split('.', 1) + bucket = ns_confs.setdefault(ns, {}) + bucket[name] = config[k] + + # I chose __enter__ and __exit__ so someday this could be + # rewritten using 'with' statement: + # for ns, handler in self.items(): + # with handler as callable: + # for k, v in ns_confs.get(ns, {}).items(): + # callable(k, v) + for ns, handler in self.items(): + exit = getattr(handler, '__exit__', None) + if exit: + callable = handler.__enter__() + no_exc = True + try: + try: + for k, v in ns_confs.get(ns, {}).items(): + callable(k, v) + except Exception: + # The exceptional case is handled here + no_exc = False + if exit is None: + raise + if not exit(*sys.exc_info()): + raise + # The exception is swallowed if exit() returns true + finally: + # The normal and non-local-goto cases are handled here + if no_exc and exit: + exit(None, None, None) + else: + for k, v in ns_confs.get(ns, {}).items(): + handler(k, v) + + def __repr__(self): + return '%s.%s(%s)' % (self.__module__, self.__class__.__name__, + dict.__repr__(self)) + + def __copy__(self): + newobj = self.__class__() + newobj.update(self) + return newobj + copy = __copy__ + + +class Config(dict): + + """A dict-like set of configuration data, with defaults and namespaces. + + May take a file, filename, or dict. + """ + + defaults = {} + environments = {} + namespaces = NamespaceSet() + + def __init__(self, file=None, **kwargs): + self.reset() + if file is not None: + self.update(file) + if kwargs: + self.update(kwargs) + + def reset(self): + """Reset self to default values.""" + self.clear() + dict.update(self, self.defaults) + + def update(self, config): + """Update self from a dict, file, or filename.""" + self._apply(Parser.load(config)) + + def _apply(self, config): + """Update self from a dict.""" + which_env = config.get('environment') + if which_env: + env = self.environments[which_env] + for k in env: + if k not in config: + config[k] = env[k] + + dict.update(self, config) + self.namespaces(config) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, v) + self.namespaces({k: v}) + + +class Parser(configparser.ConfigParser): + + """Sub-class of ConfigParser that keeps the case of options and that + raises an exception if the file cannot be read. + """ + + def optionxform(self, optionstr): + return optionstr + + def read(self, filenames): + if isinstance(filenames, text_or_bytes): + filenames = [filenames] + for filename in filenames: + # try: + # fp = open(filename) + # except IOError: + # continue + with open(filename) as fp: + self._read(fp, filename) + + def as_dict(self, raw=False, vars=None): + """Convert an INI file to a dictionary""" + # Load INI file into a dict + result = {} + for section in self.sections(): + if section not in result: + result[section] = {} + for option in self.options(section): + value = self.get(section, option, raw=raw, vars=vars) + try: + value = unrepr(value) + except Exception: + x = sys.exc_info()[1] + msg = ('Config error in section: %r, option: %r, ' + 'value: %r. Config values must be valid Python.' % + (section, option, value)) + raise ValueError(msg, x.__class__.__name__, x.args) + result[section][option] = value + return result + + def dict_from_file(self, file): + if hasattr(file, 'read'): + self.read_file(file) + else: + self.read(file) + return self.as_dict() + + @classmethod + def load(self, input): + """Resolve 'input' to dict from a dict, file, or filename.""" + is_file = ( + # Filename + isinstance(input, text_or_bytes) + # Open file object + or hasattr(input, 'read') + ) + return Parser().dict_from_file(input) if is_file else input.copy() + + +# public domain "unrepr" implementation, found on the web and then improved. + + +class _Builder: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError('unrepr does not recognize %s' % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python3 ast Node compiled from a string.""" + try: + import ast + except ImportError: + # Fallback to eval when ast package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = ast.parse('__tempvalue__ = ' + s) + return p.body[0].value + + def build_Subscript(self, o): + return self.build(o.value)[self.build(o.slice)] + + def build_Index(self, o): + return self.build(o.value) + + def _build_call35(self, o): + """ + Workaround for python 3.5 _ast.Call signature, docs found here + https://greentreesnakes.readthedocs.org/en/latest/nodes.html + """ + import ast + callee = self.build(o.func) + args = [] + if o.args is not None: + for a in o.args: + if isinstance(a, ast.Starred): + args.append(self.build(a.value)) + else: + args.append(self.build(a)) + kwargs = {} + for kw in o.keywords: + if kw.arg is None: # double asterix `**` + rst = self.build(kw.value) + if not isinstance(rst, dict): + raise TypeError('Invalid argument for call.' + 'Must be a mapping object.') + # give preference to the keys set directly from arg=value + for k, v in rst.items(): + if k not in kwargs: + kwargs[k] = v + else: # defined on the call as: arg=value + kwargs[kw.arg] = self.build(kw.value) + return callee(*args, **kwargs) + + def build_Call(self, o): + if sys.version_info >= (3, 5): + return self._build_call35(o) + + callee = self.build(o.func) + + if o.args is None: + args = () + else: + args = tuple([self.build(a) for a in o.args]) + + if o.starargs is None: + starargs = () + else: + starargs = tuple(self.build(o.starargs)) + + if o.kwargs is None: + kwargs = {} + else: + kwargs = self.build(o.kwargs) + if o.keywords is not None: # direct a=b keywords + for kw in o.keywords: + # preference because is a direct keyword against **kwargs + kwargs[kw.arg] = self.build(kw.value) + return callee(*(args + starargs), **kwargs) + + def build_List(self, o): + return list(map(self.build, o.elts)) + + def build_Str(self, o): + return o.s + + def build_Num(self, o): + return o.n + + def build_Dict(self, o): + return dict([(self.build(k), self.build(v)) + for k, v in zip(o.keys, o.values)]) + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.id + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError('unrepr could not resolve the name %s' % repr(name)) + + def build_NameConstant(self, o): + return o.value + + build_Constant = build_NameConstant # Python 3.8 change + + def build_UnaryOp(self, o): + op, operand = map(self.build, [o.op, o.operand]) + return op(operand) + + def build_BinOp(self, o): + left, op, right = map(self.build, [o.left, o.op, o.right]) + return op(left, right) + + def build_Add(self, o): + return operator.add + + def build_Mult(self, o): + return operator.mul + + def build_USub(self, o): + return operator.neg + + def build_Attribute(self, o): + parent = self.build(o.value) + return getattr(parent, o.attr) + + def build_NoneType(self, o): + return None + + +def unrepr(s): + """Return a Python object compiled from a string.""" + if not s: + return s + b = _Builder() + obj = b.astnode(s) + return b.build(obj) + + +def modules(modulePath): + """Load a module and retrieve a reference to that module.""" + __import__(modulePath) + return sys.modules[modulePath] + + +def attributes(full_attribute_name): + """Load a module and retrieve an attribute of that module.""" + + # Parse out the path, module, and attribute + last_dot = full_attribute_name.rfind('.') + attr_name = full_attribute_name[last_dot + 1:] + mod_path = full_attribute_name[:last_dot] + + mod = modules(mod_path) + # Let an AttributeError propagate outward. + try: + attr = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + # Return a reference to the attribute. + return attr diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/sessions.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/sessions.py new file mode 100644 index 000000000..0f56a4fa5 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/sessions.py @@ -0,0 +1,904 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_class = cherrypy.lib.sessions.FileSession + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory +/home/site/sessions, and the session timeout to 60 minutes. If you omit +``storage_class``, the sessions will be saved in RAM. +``tools.sessions.on`` is the only required line for working sessions, +the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. Be mindful of this default mode +for any requests that take a long time to process (streaming responses, +expensive calculations, database lookups, API calls, etc), as other concurrent +requests that also utilize sessions will hang until the session is unlocked. + +If you want to control when the session data is locked and unlocked, +set ``tools.sessions.locking = 'explicit'``. Then call +``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +`_. +However, CherryPy "recognizes" a session id by looking up the saved session +data for that id. Therefore, if you never save any session data, +**you will get a new session id for every request**. + +A side effect of CherryPy overwriting unrecognised session ids is that if you +have multiple, separate CherryPy applications running on a single domain (e.g. +on different ports), each app will overwrite the other's session id because by +default they use the same cookie name (``"session_id"``) but do not recognise +each others sessions. It is therefore a good idea to use a different name for +each, for example:: + + [/] + ... + tools.sessions.name = "my_app_session_id" + +================ +Sharing Sessions +================ + +If you run multiple instances of CherryPy (for example via mod_python behind +Apache prefork), you most likely cannot use the RAM session backend, since each +instance of CherryPy will have its own memory space. Use a different backend +instead, and verify that all instances are pointing at the same file or db +location. Alternately, you might try a load balancer which makes sessions +"sticky". Google is your friend, there. + +================ +Expiration Dates +================ + +The response cookie will possess an expiration date to inform the client at +which point to stop sending the cookie back in requests. If the server time +and client time differ, expect sessions to be unreliable. **Make sure the +system time of your server is accurate**. + +CherryPy defaults to a 60-minute session timeout, which also applies to the +cookie which is sent to the client. Unfortunately, some versions of Safari +("4 public beta" on Windows XP at least) appear to have a bug in their parsing +of the GMT expiration date--they appear to interpret the date as one hour in +the past. Sixty minutes minus one hour is pretty close to zero, so you may +experience this bug as a new session id for every request, unless the requests +are less than one second apart. To fix, try increasing the session.timeout. + +On the other extreme, some users report Firefox sending cookies after their +expiration date, although this was on a system with an inaccurate system time. +Maybe FF doesn't trust system time. +""" +import sys +import datetime +import os +import time +import threading +import binascii +import pickle + +import zc.lockfile + +import cherrypy +from cherrypy.lib import httputil +from cherrypy.lib import locking +from cherrypy.lib import is_iterator + + +missing = object() + + +class Session(object): + + """A CherryPy dict-like Session object (one per request).""" + + _id = None + + id_observers = None + "A list of callbacks to which to pass new id's." + + @property + def id(self): + """Return the current session id.""" + return self._id + + @id.setter + def id(self, value): + self._id = value + for o in self.id_observers: + o(value) + + timeout = 60 + 'Number of minutes after which to delete session data.' + + locked = False + """ + If True, this session instance has exclusive read/write access + to session data.""" + + loaded = False + """ + If True, data has been retrieved from storage. This should happen + automatically on the first attempt to access session data.""" + + clean_thread = None + 'Class-level Monitor which calls self.clean_up.' + + clean_freq = 5 + 'The poll rate for expired session cleanup in minutes.' + + originalid = None + 'The session id passed by the client. May be missing or unsafe.' + + missing = False + 'True if the session requested by the client did not exist.' + + regenerated = False + """ + True if the application called session.regenerate(). This is not set by + internal calls to regenerate the session id.""" + + debug = False + 'If True, log debug information.' + + # --------------------- Session management methods --------------------- # + + def __init__(self, id=None, **kwargs): + self.id_observers = [] + self._data = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + self.originalid = id + self.missing = False + if id is None: + if self.debug: + cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') + self._regenerate() + else: + self.id = id + if self._exists(): + if self.debug: + cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS') + else: + if self.debug: + cherrypy.log('Expired or malicious session %r; ' + 'making a new one' % id, 'TOOLS.SESSIONS') + # Expired or malicious session. Make a new one. + # See https://github.com/cherrypy/cherrypy/issues/709. + self.id = None + self.missing = True + self._regenerate() + + def now(self): + """Generate the session specific concept of 'now'. + + Other session providers can override this to use alternative, + possibly timezone aware, versions of 'now'. + """ + return datetime.datetime.now() + + def regenerate(self): + """Replace the current session (with a new id).""" + self.regenerated = True + self._regenerate() + + def _regenerate(self): + if self.id is not None: + if self.debug: + cherrypy.log( + 'Deleting the existing session %r before ' + 'regeneration.' % self.id, + 'TOOLS.SESSIONS') + self.delete() + + old_session_was_locked = self.locked + if old_session_was_locked: + self.release_lock() + if self.debug: + cherrypy.log('Old lock released.', 'TOOLS.SESSIONS') + + self.id = None + while self.id is None: + self.id = self.generate_id() + # Assert that the generated id is not already stored. + if self._exists(): + self.id = None + if self.debug: + cherrypy.log('Set id to generated %s.' % self.id, + 'TOOLS.SESSIONS') + + if old_session_was_locked: + self.acquire_lock() + if self.debug: + cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS') + + def clean_up(self): + """Clean up expired sessions.""" + pass + + def generate_id(self): + """Return a new session id.""" + return binascii.hexlify(os.urandom(20)).decode('ascii') + + def save(self): + """Save session data.""" + try: + # If session data has never been loaded then it's never been + # accessed: no need to save it + if self.loaded: + t = datetime.timedelta(seconds=self.timeout * 60) + expiration_time = self.now() + t + if self.debug: + cherrypy.log('Saving session %r with expiry %s' % + (self.id, expiration_time), + 'TOOLS.SESSIONS') + self._save(expiration_time) + else: + if self.debug: + cherrypy.log( + 'Skipping save of session %r (no session loaded).' % + self.id, 'TOOLS.SESSIONS') + finally: + if self.locked: + # Always release the lock if the user didn't release it + self.release_lock() + if self.debug: + cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS') + + def load(self): + """Copy stored session data into this session instance.""" + data = self._load() + # data is either None or a tuple (session_data, expiration_time) + if data is None or data[1] < self.now(): + if self.debug: + cherrypy.log('Expired session %r, flushing data.' % self.id, + 'TOOLS.SESSIONS') + self._data = {} + else: + if self.debug: + cherrypy.log('Data loaded for session %r.' % self.id, + 'TOOLS.SESSIONS') + self._data = data[0] + self.loaded = True + + # Stick the clean_thread in the class, not the instance. + # The instances are created and destroyed per-request. + cls = self.__class__ + if self.clean_freq and not cls.clean_thread: + # clean_up is an instancemethod and not a classmethod, + # so that tool config can be accessed inside the method. + t = cherrypy.process.plugins.Monitor( + cherrypy.engine, self.clean_up, self.clean_freq * 60, + name='Session cleanup') + t.subscribe() + cls.clean_thread = t + t.start() + if self.debug: + cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS') + + def delete(self): + """Delete stored session data.""" + self._delete() + if self.debug: + cherrypy.log('Deleted session %s.' % self.id, + 'TOOLS.SESSIONS') + + # -------------------- Application accessor methods -------------------- # + + def __getitem__(self, key): + if not self.loaded: + self.load() + return self._data[key] + + def __setitem__(self, key, value): + if not self.loaded: + self.load() + self._data[key] = value + + def __delitem__(self, key): + if not self.loaded: + self.load() + del self._data[key] + + def pop(self, key, default=missing): + """Remove the specified key and return the corresponding value. + If key is not found, default is returned if given, + otherwise KeyError is raised. + """ + if not self.loaded: + self.load() + if default is missing: + return self._data.pop(key) + else: + return self._data.pop(key, default) + + def __contains__(self, key): + if not self.loaded: + self.load() + return key in self._data + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + if not self.loaded: + self.load() + return self._data.get(key, default) + + def update(self, d): + """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" + if not self.loaded: + self.load() + self._data.update(d) + + def setdefault(self, key, default=None): + """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" + if not self.loaded: + self.load() + return self._data.setdefault(key, default) + + def clear(self): + """D.clear() -> None. Remove all items from D.""" + if not self.loaded: + self.load() + self._data.clear() + + def keys(self): + """D.keys() -> list of D's keys.""" + if not self.loaded: + self.load() + return self._data.keys() + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" + if not self.loaded: + self.load() + return self._data.items() + + def values(self): + """D.values() -> list of D's values.""" + if not self.loaded: + self.load() + return self._data.values() + + +class RamSession(Session): + + # Class-level objects. Don't rebind these! + cache = {} + locks = {} + + def clean_up(self): + """Clean up expired sessions.""" + + now = self.now() + for _id, (data, expiration_time) in self.cache.copy().items(): + if expiration_time <= now: + try: + del self.cache[_id] + except KeyError: + pass + try: + if self.locks[_id].acquire(blocking=False): + lock = self.locks.pop(_id) + lock.release() + except KeyError: + pass + + # added to remove obsolete lock objects + for _id in list(self.locks): + locked = ( + _id not in self.cache + and self.locks[_id].acquire(blocking=False) + ) + if locked: + lock = self.locks.pop(_id) + lock.release() + + def _exists(self): + return self.id in self.cache + + def _load(self): + return self.cache.get(self.id) + + def _save(self, expiration_time): + self.cache[self.id] = (self._data, expiration_time) + + def _delete(self): + self.cache.pop(self.id, None) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + return len(self.cache) + + +class FileSession(Session): + + """Implementation of the File backend for sessions + + storage_path + The folder where session data will be saved. Each session + will be saved as pickle.dump(data, expiration_time) in its own file; + the filename will be self.SESSION_PREFIX + self.id. + + lock_timeout + A timedelta or numeric seconds indicating how long + to block acquiring a lock. If None (default), acquiring a lock + will block indefinitely. + """ + + SESSION_PREFIX = 'session-' + LOCK_SUFFIX = '.lock' + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + kwargs.setdefault('lock_timeout', None) + + Session.__init__(self, id=id, **kwargs) + + # validate self.lock_timeout + if isinstance(self.lock_timeout, (int, float)): + self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) + if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): + raise ValueError( + 'Lock timeout must be numeric seconds or a timedelta instance.' + ) + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for file-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + + for k, v in kwargs.items(): + setattr(cls, k, v) + + def _get_file_path(self): + f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) + if not os.path.abspath(f).startswith(self.storage_path): + raise cherrypy.HTTPError(400, 'Invalid session id in cookie.') + return f + + def _exists(self): + path = self._get_file_path() + return os.path.exists(path) + + def _load(self, path=None): + assert self.locked, ('The session load without being locked. ' + "Check your tools' priority levels.") + if path is None: + path = self._get_file_path() + try: + with open(path, 'rb') as f: + return pickle.load(f) + except (IOError, EOFError): + e = sys.exc_info()[1] + if self.debug: + cherrypy.log('Error loading the session pickle: %s' % + e, 'TOOLS.SESSIONS') + return None + + def _save(self, expiration_time): + assert self.locked, ('The session was saved without being locked. ' + "Check your tools' priority levels.") + with open(self._get_file_path(), 'wb') as f: + pickle.dump((self._data, expiration_time), f, self.pickle_protocol) + + def _delete(self): + assert self.locked, ('The session deletion without being locked. ' + "Check your tools' priority levels.") + try: + os.unlink(self._get_file_path()) + except OSError: + pass + + def acquire_lock(self, path=None): + """Acquire an exclusive lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + path += self.LOCK_SUFFIX + checker = locking.LockChecker(self.id, self.lock_timeout) + while not checker.expired(): + try: + self.lock = zc.lockfile.LockFile(path) + except zc.lockfile.LockError: + time.sleep(0.1) + else: + break + self.locked = True + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self, path=None): + """Release the lock on the currently-loaded session data.""" + self.lock.close() + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + # Iterate over all session files in self.storage_path + for fname in os.listdir(self.storage_path): + have_session = ( + fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX) + ) + if have_session: + # We have a session file: lock and load it and check + # if it's expired. If it fails, nevermind. + path = os.path.join(self.storage_path, fname) + self.acquire_lock(path) + if self.debug: + # This is a bit of a hack, since we're calling clean_up + # on the first instance rather than the entire class, + # so depending on whether you have "debug" set on the + # path of the first session called, this may not run. + cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS') + + try: + contents = self._load(path) + # _load returns None on IOError + if contents is not None: + data, expiration_time = contents + if expiration_time < now: + # Session expired: deleting it + os.unlink(path) + finally: + self.release_lock(path) + + def __len__(self): + """Return the number of active sessions.""" + return len([fname for fname in os.listdir(self.storage_path) + if (fname.startswith(self.SESSION_PREFIX) and + not fname.endswith(self.LOCK_SUFFIX))]) + + +class MemcachedSession(Session): + + # The most popular memcached client for Python isn't thread-safe. + # Wrap all .get and .set operations in a single lock. + mc_lock = threading.RLock() + + # This is a separate set of locks per session id. + locks = {} + + servers = ['localhost:11211'] + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for memcached-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + import memcache + cls.cache = memcache.Client(cls.servers) + + def _exists(self): + self.mc_lock.acquire() + try: + return bool(self.cache.get(self.id)) + finally: + self.mc_lock.release() + + def _load(self): + self.mc_lock.acquire() + try: + return self.cache.get(self.id) + finally: + self.mc_lock.release() + + def _save(self, expiration_time): + # Send the expiration time as "Unix time" (seconds since 1/1/1970) + td = int(time.mktime(expiration_time.timetuple())) + self.mc_lock.acquire() + try: + if not self.cache.set(self.id, (self._data, expiration_time), td): + raise AssertionError( + 'Session data for id %r not set.' % self.id) + finally: + self.mc_lock.release() + + def _delete(self): + self.cache.delete(self.id) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + raise NotImplementedError + + +# Hook functions (for CherryPy tools) + +def save(): + """Save any changed session data.""" + + if not hasattr(cherrypy.serving, 'session'): + return + request = cherrypy.serving.request + response = cherrypy.serving.response + + # Guard against running twice + if hasattr(request, '_sessionsaved'): + return + request._sessionsaved = True + + if response.stream: + # If the body is being streamed, we have to save the data + # *after* the response has been written out + request.hooks.attach('on_end_request', cherrypy.session.save) + else: + # If the body is not being streamed, we save the data now + # (so we can release the lock). + if is_iterator(response.body): + response.collapse_body() + cherrypy.session.save() + + +save.failsafe = True + + +def close(): + """Close the session object for this request.""" + sess = getattr(cherrypy.serving, 'session', None) + if getattr(sess, 'locked', False): + # If the session is still locked we release the lock + sess.release_lock() + if sess.debug: + cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') + + +close.failsafe = True +close.priority = 90 + + +def init(storage_type=None, path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, clean_freq=5, + persistent=True, httponly=False, debug=False, + # Py27 compat + # *, storage_class=RamSession, + **kwargs): + """Initialize session object (using cookies). + + storage_class + The Session subclass to use. Defaults to RamSession. + + storage_type + (deprecated) + One of 'ram', 'file', memcached'. This will be + used to look up the corresponding class in cherrypy.lib.sessions + globals. For example, 'file' will use the FileSession class. + + path + The 'path' value to stick in the response cookie metadata. + + path_header + If 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + The name of the cookie. + + timeout + The expiration timeout (in minutes) for the stored session data. + If 'persistent' is True (the default), this is also the timeout + for the cookie. + + domain + The cookie domain. + + secure + If False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + clean_freq (minutes) + The poll rate for expired session cleanup. + + persistent + If True (the default), the 'timeout' argument will be used + to expire the cookie. If False, the cookie will not have an expiry, + and the cookie will be a "session cookie" which expires when the + browser is closed. + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + Any additional kwargs will be bound to the new Session instance, + and may be specific to the storage type. See the subclass of Session + you're using for more information. + """ + + # Py27 compat + storage_class = kwargs.pop('storage_class', RamSession) + + request = cherrypy.serving.request + + # Guard against running twice + if hasattr(request, '_session_init_flag'): + return + request._session_init_flag = True + + # Check if request came with a session ID + id = None + if name in request.cookie: + id = request.cookie[name].value + if debug: + cherrypy.log('ID obtained from request.cookie: %r' % id, + 'TOOLS.SESSIONS') + + first_time = not hasattr(cherrypy, 'session') + + if storage_type: + if first_time: + msg = 'storage_type is deprecated. Supply storage_class instead' + cherrypy.log(msg) + storage_class = storage_type.title() + 'Session' + storage_class = globals()[storage_class] + + # call setup first time only + if first_time: + if hasattr(storage_class, 'setup'): + storage_class.setup(**kwargs) + + # Create and attach a new Session instance to cherrypy.serving. + # It will possess a reference to (and lock, and lazily load) + # the requested session data. + kwargs['timeout'] = timeout + kwargs['clean_freq'] = clean_freq + cherrypy.serving.session = sess = storage_class(id, **kwargs) + sess.debug = debug + + def update_cookie(id): + """Update the cookie every time the session id changes.""" + cherrypy.serving.response.cookie[name] = id + sess.id_observers.append(update_cookie) + + # Create cherrypy.session which will proxy to cherrypy.serving.session + if not hasattr(cherrypy, 'session'): + cherrypy.session = cherrypy._ThreadLocalProxy('session') + + if persistent: + cookie_timeout = timeout + else: + # See http://support.microsoft.com/kb/223799/EN-US/ + # and http://support.mozilla.com/en-US/kb/Cookies + cookie_timeout = None + set_response_cookie(path=path, path_header=path_header, name=name, + timeout=cookie_timeout, domain=domain, secure=secure, + httponly=httponly) + + +def set_response_cookie(path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, httponly=False): + """Set a response cookie for the client. + + path + the 'path' value to stick in the response cookie metadata. + + path_header + if 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + the name of the cookie. + + timeout + the expiration timeout for the cookie. If 0 or other boolean + False, no 'expires' param will be set, and the cookie will be a + "session cookie" which expires when the browser is closed. + + domain + the cookie domain. + + secure + if False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + """ + # Set response cookie + cookie = cherrypy.serving.response.cookie + cookie[name] = cherrypy.serving.session.id + cookie[name]['path'] = ( + path or + cherrypy.serving.request.headers.get(path_header) or + '/' + ) + + if timeout: + cookie[name]['max-age'] = timeout * 60 + _add_MSIE_max_age_workaround(cookie[name], timeout) + if domain is not None: + cookie[name]['domain'] = domain + if secure: + cookie[name]['secure'] = 1 + if httponly: + if not cookie[name].isReservedKey('httponly'): + raise ValueError('The httponly cookie token is not supported.') + cookie[name]['httponly'] = 1 + + +def _add_MSIE_max_age_workaround(cookie, timeout): + """ + We'd like to use the "max-age" param as indicated in + http://www.faqs.org/rfcs/rfc2109.html but IE doesn't + save it to disk and the session is lost if people close + the browser. So we have to use the old "expires" ... sigh ... + """ + expires = time.time() + timeout * 60 + cookie['expires'] = httputil.HTTPDate(expires) + + +def expire(): + """Expire the current session cookie.""" + name = cherrypy.serving.request.config.get( + 'tools.sessions.name', 'session_id') + one_year = 60 * 60 * 24 * 365 + e = time.time() - one_year + cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) + cherrypy.serving.response.cookie[name].pop('max-age', None) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/static.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/static.py new file mode 100644 index 000000000..c1ad95f3d --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/static.py @@ -0,0 +1,418 @@ +"""Module with helpers for serving static files.""" + +import mimetypes +import os +import platform +import re +import stat +import unicodedata +import urllib.parse +from email.generator import _make_boundary as make_boundary +from io import UnsupportedOperation + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import cptools, file_generator_limited, httputil + + +def _setup_mimetypes(): + """Pre-initialize global mimetype map.""" + if not mimetypes.inited: + mimetypes.init() + mimetypes.types_map['.dwg'] = 'image/x-dwg' + mimetypes.types_map['.ico'] = 'image/x-icon' + mimetypes.types_map['.bz2'] = 'application/x-bzip2' + mimetypes.types_map['.gz'] = 'application/x-gzip' + + +_setup_mimetypes() + + +def _make_content_disposition(disposition, file_name): + """Create HTTP header for downloading a file with a UTF-8 filename. + + This function implements the recommendations of :rfc:`6266#appendix-D`. + See this and related answers: https://stackoverflow.com/a/8996249/2173868. + """ + # As normalization algorithm for `unicodedata` is used composed form (NFC + # and NFKC) with compatibility equivalence criteria (NFK), so "NFKC" is the + # one. It first applies the compatibility decomposition, followed by the + # canonical composition. Should be displayed in the same manner, should be + # treated in the same way by applications such as alphabetizing names or + # searching, and may be substituted for each other. + # See: https://en.wikipedia.org/wiki/Unicode_equivalence. + ascii_name = ( + unicodedata.normalize('NFKC', file_name). + encode('ascii', errors='ignore').decode() + ) + header = '{}; filename="{}"'.format(disposition, ascii_name) + if ascii_name != file_name: + quoted_name = urllib.parse.quote(file_name) + header += '; filename*=UTF-8\'\'{}'.format(quoted_name) + return header + + +def serve_file(path, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=; filename*=utf-8''" + as described in :rfc:`6266#appendix-D`. + If name is None, it will be set to the basename of path. + If disposition is None, no Content-Disposition header will be written. + """ + response = cherrypy.serving.response + + # If path is relative, users should fix it by making path absolute. + # That is, CherryPy should not guess where the application root is. + # It certainly should *not* use cwd (since CP may be invoked from a + # variety of paths). If using tools.staticdir, you can make your relative + # paths become absolute by supplying a value for "tools.staticdir.root". + if not os.path.isabs(path): + msg = "'%s' is not an absolute path." % path + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + + try: + st = os.stat(path) + except (OSError, TypeError, ValueError): + # OSError when file fails to stat + # TypeError on Python 2 when there's a null byte + # ValueError on Python 3 when there's a null byte + if debug: + cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Check if path is a directory. + if stat.S_ISDIR(st.st_mode): + # Let the caller deal with it as they like. + if debug: + cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + + if content_type is None: + # Set content-type based on filename extension + ext = '' + i = path.rfind('.') + if i != -1: + ext = path[i:].lower() + content_type = mimetypes.types_map.get(ext, None) + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + name = os.path.basename(path) + cd = _make_content_disposition(disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + content_length = st.st_size + fileobj = open(path, 'rb') + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given file object. + + The Content-Type header will be set to the content_type arg, if provided. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=; filename*=utf-8''" + as described in :rfc:`6266#appendix-D`. + If name is None, 'filename' will not be set. + If disposition is None, no Content-Disposition header will be written. + + CAUTION: If the request contains a 'Range' header, one or more seek()s will + be performed on the file object. This may cause undesired behavior if + the file object is not seekable. It could also produce undesired results + if the caller set the read position of the file object prior to calling + serve_fileobj(), expecting that the data would be served starting from that + position. + """ + response = cherrypy.serving.response + + try: + st = os.fstat(fileobj.fileno()) + except AttributeError: + if debug: + cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') + content_length = None + except UnsupportedOperation: + content_length = None + else: + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + content_length = st.st_size + + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + cd = disposition + else: + cd = _make_content_disposition(disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def _serve_fileobj(fileobj, content_type, content_length, debug=False): + """Set ``response.body`` to the given file object, perhaps ranged. + + Internal helper. + """ + response = cherrypy.serving.response + + # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code + request = cherrypy.serving.request + if request.protocol >= (1, 1): + response.headers['Accept-Ranges'] = 'bytes' + r = httputil.get_ranges(request.headers.get('Range'), content_length) + if r == []: + response.headers['Content-Range'] = 'bytes */%s' % content_length + message = ('Invalid Range (first-byte-pos greater than ' + 'Content-Length)') + if debug: + cherrypy.log(message, 'TOOLS.STATIC') + raise cherrypy.HTTPError(416, message) + + if r: + if len(r) == 1: + # Return a single-part response. + start, stop = r[0] + if stop > content_length: + stop = content_length + r_len = stop - start + if debug: + cherrypy.log( + 'Single part; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + response.status = '206 Partial Content' + response.headers['Content-Range'] = ( + 'bytes %s-%s/%s' % (start, stop - 1, content_length)) + response.headers['Content-Length'] = r_len + fileobj.seek(start) + response.body = file_generator_limited(fileobj, r_len) + else: + # Return a multipart/byteranges response. + response.status = '206 Partial Content' + boundary = make_boundary() + ct = 'multipart/byteranges; boundary=%s' % boundary + response.headers['Content-Type'] = ct + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + def file_ranges(): + # Apache compatibility: + yield b'\r\n' + + for start, stop in r: + if debug: + cherrypy.log( + 'Multipart; start: %r, stop: %r' % ( + start, stop), + 'TOOLS.STATIC') + yield ntob('--' + boundary, 'ascii') + yield ntob('\r\nContent-type: %s' % content_type, + 'ascii') + yield ntob( + '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % ( + start, stop - 1, content_length), + 'ascii') + fileobj.seek(start) + gen = file_generator_limited(fileobj, stop - start) + for chunk in gen: + yield chunk + yield b'\r\n' + # Final boundary + yield ntob('--' + boundary + '--', 'ascii') + + # Apache compatibility: + yield b'\r\n' + response.body = file_ranges() + return response.body + else: + if debug: + cherrypy.log('No byteranges requested', 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + response.headers['Content-Length'] = content_length + response.body = fileobj + return response.body + + +def serve_download(path, name=None): + """Serve 'path' as an application/x-download attachment.""" + # This is such a common idiom I felt it deserved its own wrapper. + return serve_file(path, 'application/x-download', 'attachment', name) + + +def _attempt(filename, content_types, debug=False): + if debug: + cherrypy.log('Attempting %r (content_types %r)' % + (filename, content_types), 'TOOLS.STATICDIR') + try: + # you can set the content types for a + # complete directory per extension + content_type = None + if content_types: + r, ext = os.path.splitext(filename) + content_type = content_types.get(ext[1:], None) + serve_file(filename, content_type=content_type, debug=debug) + return True + except cherrypy.NotFound: + # If we didn't find the static file, continue handling the + # request. We might find a dynamic handler instead. + if debug: + cherrypy.log('NotFound', 'TOOLS.STATICFILE') + return False + + +def staticdir(section, dir, root='', match='', content_types=None, index='', + debug=False): + """Serve a static resource from the given (root +) dir. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + index + If provided, it should be the (relative) name of a file to + serve for directory requests. For example, if the dir argument is + '/home/me', the Request-URI is 'myapp', and the index arg is + 'index.html', the file '/home/me/myapp/index.html' will be sought. + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICDIR') + return False + + # Allow the use of '~' to refer to a user's home directory. + dir = os.path.expanduser(dir) + + # If dir is relative, make absolute using "root". + if not os.path.isabs(dir): + if not root: + msg = 'Static dir requires an absolute dir (or root).' + if debug: + cherrypy.log(msg, 'TOOLS.STATICDIR') + raise ValueError(msg) + dir = os.path.join(root, dir) + + # Determine where we are in the object tree relative to 'section' + # (where the static tool was defined). + if section == 'global': + section = '/' + section = section.rstrip(r'\/') + branch = request.path_info[len(section) + 1:] + branch = urllib.parse.unquote(branch.lstrip(r'\/')) + + # Requesting a file in sub-dir of the staticdir results + # in mixing of delimiter styles, e.g. C:\static\js/script.js. + # Windows accepts this form except not when the path is + # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js. + # http://bit.ly/1vdioCX + if platform.system() == 'Windows': + branch = branch.replace('/', '\\') + + # If branch is "", filename will end in a slash + filename = os.path.join(dir, branch) + if debug: + cherrypy.log('Checking file %r to fulfill %r' % + (filename, request.path_info), 'TOOLS.STATICDIR') + + # There's a chance that the branch pulled from the URL might + # have ".." or similar uplevel attacks in it. Check that the final + # filename is a child of dir. + if not os.path.normpath(filename).startswith(os.path.normpath(dir)): + raise cherrypy.HTTPError(403) # Forbidden + + handled = _attempt(filename, content_types) + if not handled: + # Check for an index file if a folder was requested. + if index: + handled = _attempt(os.path.join(filename, index), content_types) + if handled: + request.is_index = filename[-1] in (r'\/') + return handled + + +def staticfile(filename, root=None, match='', content_types=None, debug=False): + """Serve a static resource from the given (root +) filename. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICFILE') + return False + + # If filename is relative, make absolute using "root". + if not os.path.isabs(filename): + if not root: + msg = "Static tool requires an absolute filename (got '%s')." % ( + filename,) + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + filename = os.path.join(root, filename) + + return _attempt(filename, content_types, debug=debug) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/xmlrpcutil.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 000000000..29d9c4a2b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/lib/xmlrpcutil.py @@ -0,0 +1,60 @@ +"""XML-RPC tool helpers.""" +import sys +from xmlrpc.client import ( + loads as xmlrpc_loads, dumps as xmlrpc_dumps, + Fault as XMLRPCFault +) + +import cherrypy +from cherrypy._cpcompat import ntob + + +def process_body(): + """Return (params, method) from request body.""" + try: + return xmlrpc_loads(cherrypy.request.body.read()) + except Exception: + return ('ERROR PARAMS', ), 'ERRORMETHOD' + + +def patched_path(path): + """Return 'path', doctored for RPC.""" + if not path.endswith('/'): + path += '/' + if path.startswith('/RPC2/'): + # strip the first /rpc2 + path = path[5:] + return path + + +def _set_response(body): + """Set up HTTP status, headers and body within CherryPy.""" + # The XML-RPC spec (http://www.xmlrpc.com/spec) says: + # "Unless there's a lower-level error, always return 200 OK." + # Since Python's xmlrpc_client interprets a non-200 response + # as a "Protocol Error", we'll just return 200 every time. + response = cherrypy.response + response.status = '200 OK' + response.body = ntob(body, 'utf-8') + response.headers['Content-Type'] = 'text/xml' + response.headers['Content-Length'] = len(body) + + +def respond(body, encoding='utf-8', allow_none=0): + """Construct HTTP response body.""" + if not isinstance(body, XMLRPCFault): + body = (body,) + + _set_response( + xmlrpc_dumps( + body, methodresponse=1, + encoding=encoding, + allow_none=allow_none + ) + ) + + +def on_error(*args, **kwargs): + """Construct HTTP response body for an error response.""" + body = str(sys.exc_info()[1]) + _set_response(xmlrpc_dumps(XMLRPCFault(1, body))) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__init__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__init__.py new file mode 100644 index 000000000..f242d226a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__init__.py @@ -0,0 +1,17 @@ +"""Site container for an HTTP server. + +A Web Site Process Bus object is used to connect applications, servers, +and frameworks with site-wide services such as daemonization, process +reload, signal handling, drop privileges, PID file management, logging +for all of these, and many more. + +The 'plugins' module defines a few abstract and concrete services for +use with the bus. Some use tool-specific channels; see the documentation +for each class. +""" + +from .wspbus import bus +from . import plugins, servers + + +__all__ = ('bus', 'plugins', 'servers') diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..5f2aa769f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/plugins.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/plugins.cpython-310.pyc new file mode 100644 index 000000000..ca4175a6c Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/plugins.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/servers.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/servers.cpython-310.pyc new file mode 100644 index 000000000..16fd89271 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/servers.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/win32.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/win32.cpython-310.pyc new file mode 100644 index 000000000..3aad81273 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/win32.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/wspbus.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/wspbus.cpython-310.pyc new file mode 100644 index 000000000..681e91a7d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/__pycache__/wspbus.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/plugins.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/plugins.py new file mode 100644 index 000000000..e96fb1ce2 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/plugins.py @@ -0,0 +1,755 @@ +"""Site services for use with a Web Site Process Bus.""" + +import os +import re +import signal as _signal +import sys +import time +import threading +import _thread + +from cherrypy._cpcompat import text_or_bytes +from cherrypy._cpcompat import ntob + +# _module__file__base is used by Autoreload to make +# absolute any filenames retrieved from sys.modules which are not +# already absolute paths. This is to work around Python's quirk +# of importing the startup script and using a relative filename +# for it in sys.modules. +# +# Autoreload examines sys.modules afresh every time it runs. If an application +# changes the current directory by executing os.chdir(), then the next time +# Autoreload runs, it will not be able to find any filenames which are +# not absolute paths, because the current directory is not the same as when the +# module was first imported. Autoreload will then wrongly conclude the file +# has "changed", and initiate the shutdown/re-exec sequence. +# See ticket #917. +# For this workaround to have a decent probability of success, this module +# needs to be imported as early as possible, before the app has much chance +# to change the working directory. +_module__file__base = os.getcwd() + + +class SimplePlugin(object): + + """Plugin base class which auto-subscribes methods for known channels.""" + + bus = None + """A :class:`Bus `, usually cherrypy.engine. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Register this object as a (multi-channel) listener on the bus.""" + for channel in self.bus.listeners: + # Subscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.subscribe(channel, method) + + def unsubscribe(self): + """Unregister this object as a listener on the bus.""" + for channel in self.bus.listeners: + # Unsubscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.unsubscribe(channel, method) + + +class SignalHandler(object): + + """Register bus channels (and listeners) for system signals. + + You can modify what signals your application listens for, and what it does + when it receives signals, by modifying :attr:`SignalHandler.handlers`, + a dict of {signal name: callback} pairs. The default set is:: + + handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + The :func:`SignalHandler.handle_SIGHUP`` method calls + :func:`bus.restart()` + if the process is daemonized, but + :func:`bus.exit()` + if the process is attached to a TTY. This is because Unix window + managers tend to send SIGHUP to terminal windows when the user closes them. + + Feel free to add signals which are not available on every platform. + The :class:`SignalHandler` will ignore errors raised from attempting + to register handlers for unknown signals. + """ + + handlers = {} + """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" + + signals = {} + """A map from signal numbers to names.""" + + for k, v in vars(_signal).items(): + if k.startswith('SIG') and not k.startswith('SIG_'): + signals[v] = k + del k, v + + def __init__(self, bus): + self.bus = bus + # Set default handlers + self.handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + if sys.platform[:4] == 'java': + del self.handlers['SIGUSR1'] + self.handlers['SIGUSR2'] = self.bus.graceful + self.bus.log('SIGUSR1 cannot be set on the JVM platform. ' + 'Using SIGUSR2 instead.') + self.handlers['SIGINT'] = self._jython_SIGINT_handler + + self._previous_handlers = {} + # used to determine is the process is a daemon in `self._is_daemonized` + self._original_pid = os.getpid() + + def _jython_SIGINT_handler(self, signum=None, frame=None): + # See http://bugs.jython.org/issue1313 + self.bus.log('Keyboard Interrupt: shutting down bus') + self.bus.exit() + + def _is_daemonized(self): + """Return boolean indicating if the current process is + running as a daemon. + + The criteria to determine the `daemon` condition is to verify + if the current pid is not the same as the one that got used on + the initial construction of the plugin *and* the stdin is not + connected to a terminal. + + The sole validation of the tty is not enough when the plugin + is executing inside other process like in a CI tool + (Buildbot, Jenkins). + """ + return ( + self._original_pid != os.getpid() and + not os.isatty(sys.stdin.fileno()) + ) + + def subscribe(self): + """Subscribe self.handlers to signals.""" + for sig, func in self.handlers.items(): + try: + self.set_handler(sig, func) + except ValueError: + pass + + def unsubscribe(self): + """Unsubscribe self.handlers from signals.""" + for signum, handler in self._previous_handlers.items(): + signame = self.signals[signum] + + if handler is None: + self.bus.log('Restoring %s handler to SIG_DFL.' % signame) + handler = _signal.SIG_DFL + else: + self.bus.log('Restoring %s handler %r.' % (signame, handler)) + + try: + our_handler = _signal.signal(signum, handler) + if our_handler is None: + self.bus.log('Restored old %s handler %r, but our ' + 'handler was not registered.' % + (signame, handler), level=30) + except ValueError: + self.bus.log('Unable to restore %s handler %r.' % + (signame, handler), level=40, traceback=True) + + def set_handler(self, signal, listener=None): + """Subscribe a handler for the given signal (number or name). + + If the optional 'listener' argument is provided, it will be + subscribed as a listener for the given signal's channel. + + If the given signal name or number is not available on the current + platform, ValueError is raised. + """ + if isinstance(signal, text_or_bytes): + signum = getattr(_signal, signal, None) + if signum is None: + raise ValueError('No such signal: %r' % signal) + signame = signal + else: + try: + signame = self.signals[signal] + except KeyError: + raise ValueError('No such signal: %r' % signal) + signum = signal + + prev = _signal.signal(signum, self._handle_signal) + self._previous_handlers[signum] = prev + + if listener is not None: + self.bus.log('Listening for %s.' % signame) + self.bus.subscribe(signame, listener) + + def _handle_signal(self, signum=None, frame=None): + """Python signal handler (self.set_handler subscribes it for you).""" + signame = self.signals[signum] + self.bus.log('Caught signal %s.' % signame) + self.bus.publish(signame) + + def handle_SIGHUP(self): + """Restart if daemonized, else exit.""" + if self._is_daemonized(): + self.bus.log('SIGHUP caught while daemonized. Restarting.') + self.bus.restart() + else: + # not daemonized (may be foreground or background) + self.bus.log('SIGHUP caught but not daemonized. Exiting.') + self.bus.exit() + + +try: + import pwd + import grp +except ImportError: + pwd, grp = None, None + + +class DropPrivileges(SimplePlugin): + + """Drop privileges. uid/gid arguments not available on Windows. + + Special thanks to `Gavin Baker + `_ + """ + + def __init__(self, bus, umask=None, uid=None, gid=None): + SimplePlugin.__init__(self, bus) + self.finalized = False + self.uid = uid + self.gid = gid + self.umask = umask + + @property + def uid(self): + """The uid under which to run. Availability: Unix.""" + return self._uid + + @uid.setter + def uid(self, val): + if val is not None: + if pwd is None: + self.bus.log('pwd module not available; ignoring uid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = pwd.getpwnam(val)[2] + self._uid = val + + @property + def gid(self): + """The gid under which to run. Availability: Unix.""" + return self._gid + + @gid.setter + def gid(self, val): + if val is not None: + if grp is None: + self.bus.log('grp module not available; ignoring gid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = grp.getgrnam(val)[2] + self._gid = val + + @property + def umask(self): + """The default permission mode for newly created files and directories. + + Usually expressed in octal format, for example, ``0644``. + Availability: Unix, Windows. + """ + return self._umask + + @umask.setter + def umask(self, val): + if val is not None: + try: + os.umask + except AttributeError: + self.bus.log('umask function not available; ignoring umask.', + level=30) + val = None + self._umask = val + + def start(self): + # uid/gid + def current_ids(): + """Return the current (uid, gid) if available.""" + name, group = None, None + if pwd: + name = pwd.getpwuid(os.getuid())[0] + if grp: + group = grp.getgrgid(os.getgid())[0] + return name, group + + if self.finalized: + if not (self.uid is None and self.gid is None): + self.bus.log('Already running as uid: %r gid: %r' % + current_ids()) + else: + if self.uid is None and self.gid is None: + if pwd or grp: + self.bus.log('uid/gid not set', level=30) + else: + self.bus.log('Started as uid: %r gid: %r' % current_ids()) + if self.gid is not None: + os.setgid(self.gid) + os.setgroups([]) + if self.uid is not None: + os.setuid(self.uid) + self.bus.log('Running as uid: %r gid: %r' % current_ids()) + + # umask + if self.finalized: + if self.umask is not None: + self.bus.log('umask already set to: %03o' % self.umask) + else: + if self.umask is None: + self.bus.log('umask not set', level=30) + else: + old_umask = os.umask(self.umask) + self.bus.log('umask old: %03o, new: %03o' % + (old_umask, self.umask)) + + self.finalized = True + # This is slightly higher than the priority for server.start + # in order to facilitate the most common use: starting on a low + # port (which requires root) and then dropping to another user. + start.priority = 77 + + +class Daemonizer(SimplePlugin): + + """Daemonize the running script. + + Use this with a Web Site Process Bus via:: + + Daemonizer(bus).subscribe() + + When this component finishes, the process is completely decoupled from + the parent environment. Please note that when this component is used, + the return code from the parent process will still be 0 if a startup + error occurs in the forked children. Errors in the initial daemonizing + process still return proper exit codes. Therefore, if you use this + plugin to daemonize, don't use the return code as an accurate indicator + of whether the process fully started. In fact, that return code only + indicates if the process successfully finished the first fork. + """ + + def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', + stderr='/dev/null'): + SimplePlugin.__init__(self, bus) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.finalized = False + + def start(self): + if self.finalized: + self.bus.log('Already deamonized.') + + # forking has issues with threads: + # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html + # "The general problem with making fork() work in a multi-threaded + # world is what to do with all of the threads..." + # So we check for active threads: + if threading.active_count() != 1: + self.bus.log('There are %r active threads. ' + 'Daemonizing now may cause strange failures.' % + threading.enumerate(), level=30) + + self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log) + + self.finalized = True + start.priority = 65 + + @staticmethod + def daemonize( + stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', + logger=lambda msg: None): + # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) + # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 + + # Finish up with the current stdout/stderr + sys.stdout.flush() + sys.stderr.flush() + + error_tmpl = ( + '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n' + ) + + for fork in range(2): + msg = ['Forking once.', 'Forking twice.'][fork] + try: + pid = os.fork() + if pid > 0: + # This is the parent; exit. + logger(msg) + os._exit(0) + except OSError as exc: + # Python raises OSError rather than returning negative numbers. + sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1)) + if fork == 0: + os.setsid() + + os.umask(0) + + si = open(stdin, 'r') + so = open(stdout, 'a+') + se = open(stderr, 'a+') + + # os.dup2(fd, fd2) will close fd2 if necessary, + # so we don't explicitly close stdin/out/err. + # See http://docs.python.org/lib/os-fd-ops.html + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + logger('Daemonized to PID: %s' % os.getpid()) + + +class PIDFile(SimplePlugin): + + """Maintain a PID file via a WSPBus.""" + + def __init__(self, bus, pidfile): + SimplePlugin.__init__(self, bus) + self.pidfile = pidfile + self.finalized = False + + def start(self): + pid = os.getpid() + if self.finalized: + self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) + else: + with open(self.pidfile, 'wb') as f: + f.write(ntob('%s\n' % pid, 'utf8')) + self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) + self.finalized = True + start.priority = 70 + + def exit(self): + try: + os.remove(self.pidfile) + self.bus.log('PID file removed: %r.' % self.pidfile) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + pass + + +class PerpetualTimer(threading.Timer): + + """A responsive subclass of threading.Timer whose run() method repeats. + + Use this timer only when you really need a very interruptible timer; + this checks its 'finished' condition up to 20 times a second, which can + results in pretty high CPU usage + """ + + def __init__(self, *args, **kwargs): + "Override parent constructor to allow 'bus' to be provided." + self.bus = kwargs.pop('bus', None) + super(PerpetualTimer, self).__init__(*args, **kwargs) + + def run(self): + while True: + self.finished.wait(self.interval) + if self.finished.isSet(): + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log( + 'Error in perpetual timer thread function %r.' % + self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class BackgroundTask(threading.Thread): + + """A subclass of threading.Thread whose run() method repeats. + + Use this class for most repeating tasks. It uses time.sleep() to wait + for each interval, which isn't very responsive; that is, even if you call + self.cancel(), you'll have to wait until the sleep() call finishes before + the thread stops. To compensate, it defaults to being daemonic, which means + it won't delay stopping the whole process. + """ + + def __init__(self, interval, function, args=[], kwargs={}, bus=None): + super(BackgroundTask, self).__init__() + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.running = False + self.bus = bus + + # default to daemonic + self.daemon = True + + def cancel(self): + self.running = False + + def run(self): + self.running = True + while self.running: + time.sleep(self.interval) + if not self.running: + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log('Error in background task thread function %r.' + % self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class Monitor(SimplePlugin): + + """WSPBus listener to periodically run a callback in its own thread.""" + + callback = None + """The function to call at intervals.""" + + frequency = 60 + """The time in seconds between callback runs.""" + + thread = None + """A :class:`BackgroundTask` + thread. + """ + + def __init__(self, bus, callback, frequency=60, name=None): + SimplePlugin.__init__(self, bus) + self.callback = callback + self.frequency = frequency + self.thread = None + self.name = name + + def start(self): + """Start our callback in its own background thread.""" + if self.frequency > 0: + threadname = self.name or self.__class__.__name__ + if self.thread is None: + self.thread = BackgroundTask(self.frequency, self.callback, + bus=self.bus) + self.thread.name = threadname + self.thread.start() + self.bus.log('Started monitor thread %r.' % threadname) + else: + self.bus.log('Monitor thread %r already started.' % threadname) + start.priority = 70 + + def stop(self): + """Stop our callback's background task thread.""" + if self.thread is None: + self.bus.log('No thread running for %s.' % + self.name or self.__class__.__name__) + else: + if self.thread is not threading.current_thread(): + name = self.thread.name + self.thread.cancel() + if not self.thread.daemon: + self.bus.log('Joining %r' % name) + self.thread.join() + self.bus.log('Stopped thread %r.' % name) + self.thread = None + + def graceful(self): + """Stop the callback's background task thread and restart it.""" + self.stop() + self.start() + + +class Autoreloader(Monitor): + + """Monitor which re-executes the process when files change. + + This :ref:`plugin` restarts the process (via :func:`os.execv`) + if any of the files it monitors change (or is deleted). By default, the + autoreloader monitors all imported modules; you can add to the + set by adding to ``autoreload.files``:: + + cherrypy.engine.autoreload.files.add(myFile) + + If there are imported files you do *not* wish to monitor, you can + adjust the ``match`` attribute, a regular expression. For example, + to stop monitoring cherrypy itself:: + + cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' + + Like all :class:`Monitor` plugins, + the autoreload plugin takes a ``frequency`` argument. The default is + 1 second; that is, the autoreloader will examine files once each second. + """ + + files = None + """The set of files to poll for modifications.""" + + frequency = 1 + """The interval in seconds at which to poll for modified files.""" + + match = '.*' + """A regular expression by which to match filenames.""" + + def __init__(self, bus, frequency=1, match='.*'): + self.mtimes = {} + self.files = set() + self.match = match + Monitor.__init__(self, bus, self.run, frequency) + + def start(self): + """Start our own background task thread for self.run.""" + if self.thread is None: + self.mtimes = {} + Monitor.start(self) + start.priority = 70 + + def sysfiles(self): + """Return a Set of sys.modules filenames to monitor.""" + search_mod_names = filter( + re.compile(self.match).match, + list(sys.modules.keys()), + ) + mods = map(sys.modules.get, search_mod_names) + return set(filter(None, map(self._file_for_module, mods))) + + @classmethod + def _file_for_module(cls, module): + """Return the relevant file for the module.""" + return ( + cls._archive_for_zip_module(module) + or cls._file_for_file_module(module) + ) + + @staticmethod + def _archive_for_zip_module(module): + """Return the archive filename for the module if relevant.""" + try: + return module.__loader__.archive + except AttributeError: + pass + + @classmethod + def _file_for_file_module(cls, module): + """Return the file for the module.""" + try: + return module.__file__ and cls._make_absolute(module.__file__) + except AttributeError: + pass + + @staticmethod + def _make_absolute(filename): + """Ensure filename is absolute to avoid effect of os.chdir.""" + return filename if os.path.isabs(filename) else ( + os.path.normpath(os.path.join(_module__file__base, filename)) + ) + + def run(self): + """Reload the process if registered files have been modified.""" + for filename in self.sysfiles() | self.files: + if filename: + if filename.endswith('.pyc'): + filename = filename[:-1] + + oldtime = self.mtimes.get(filename, 0) + if oldtime is None: + # Module with no .py file. Skip it. + continue + + try: + mtime = os.stat(filename).st_mtime + except OSError: + # Either a module with no .py file, or it's been deleted. + mtime = None + + if filename not in self.mtimes: + # If a module has no .py file, this will be None. + self.mtimes[filename] = mtime + else: + if mtime is None or mtime > oldtime: + # The file has been deleted or modified. + self.bus.log('Restarting because %s changed.' % + filename) + self.thread.cancel() + self.bus.log('Stopped thread %r.' % + self.thread.name) + self.bus.restart() + return + + +class ThreadManager(SimplePlugin): + + """Manager for HTTP request threads. + + If you have control over thread creation and destruction, publish to + the 'acquire_thread' and 'release_thread' channels (for each thread). + This will register/unregister the current thread and publish to + 'start_thread' and 'stop_thread' listeners in the bus as needed. + + If threads are created and destroyed by code you do not control + (e.g., Apache), then, at the beginning of every HTTP request, + publish to 'acquire_thread' only. You should not publish to + 'release_thread' in this case, since you do not know whether + the thread will be re-used or not. The bus will call + 'stop_thread' listeners for you when it stops. + """ + + threads = None + """A map of {thread ident: index number} pairs.""" + + def __init__(self, bus): + self.threads = {} + SimplePlugin.__init__(self, bus) + self.bus.listeners.setdefault('acquire_thread', set()) + self.bus.listeners.setdefault('start_thread', set()) + self.bus.listeners.setdefault('release_thread', set()) + self.bus.listeners.setdefault('stop_thread', set()) + + def acquire_thread(self): + """Run 'start_thread' listeners for the current thread. + + If the current thread has already been seen, any 'start_thread' + listeners will not be run again. + """ + thread_ident = _thread.get_ident() + if thread_ident not in self.threads: + # We can't just use get_ident as the thread ID + # because some platforms reuse thread ID's. + i = len(self.threads) + 1 + self.threads[thread_ident] = i + self.bus.publish('start_thread', i) + + def release_thread(self): + """Release the current thread and run 'stop_thread' listeners.""" + thread_ident = _thread.get_ident() + i = self.threads.pop(thread_ident, None) + if i is not None: + self.bus.publish('stop_thread', i) + + def stop(self): + """Release all threads and run all 'stop_thread' listeners.""" + for thread_ident, i in self.threads.items(): + self.bus.publish('stop_thread', i) + self.threads.clear() + graceful = stop diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/servers.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/servers.py new file mode 100644 index 000000000..717a8de0f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/servers.py @@ -0,0 +1,416 @@ +r""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin`. It's an instance of +:class:`cherrypy._cpserver.Server`, which is a subclass of +:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class +is designed to control other servers, as well. + +Multiple servers/ports +====================== + +If you need to start more than one HTTP server (to serve on multiple ports, or +protocols, etc.), you can manually register each one and then start them all +with engine.start:: + + s1 = ServerAdapter( + cherrypy.engine, + MyWSGIServer(host='0.0.0.0', port=80) + ) + s2 = ServerAdapter( + cherrypy.engine, + another.HTTPServer(host='127.0.0.1', SSL=True) + ) + s1.subscribe() + s2.subscribe() + cherrypy.engine.start() + +.. index:: SCGI + +FastCGI/SCGI +============ + +There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in +:mod:`cherrypy.process.servers`. To start an fcgi server, for example, +wrap an instance of it in a ServerAdapter:: + + addr = ('0.0.0.0', 4000) + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) + s.subscribe() + +The :doc:`cherryd` startup script will do the above for +you via its `-f` flag. +Note that you need to download and install `flup `_ +yourself, whether you use ``cherryd`` or not. + +.. _fastcgi: +.. index:: FastCGI + +FastCGI +------- + +A very simple setup lets your cherry run with FastCGI. +You just need the flup library, +plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. + +CherryPy code +^^^^^^^^^^^^^ + +hello.py:: + + #!/usr/bin/python + import cherrypy + + class HelloWorld: + '''Sample request handler class.''' + @cherrypy.expose + def index(self): + return "Hello world!" + + cherrypy.tree.mount(HelloWorld()) + # CherryPy autoreload must be disabled for the flup server to work + cherrypy.config.update({'engine.autoreload.on':False}) + +Then run :doc:`/deployguide/cherryd` with the '-f' arg:: + + cherryd -c -d -f -i hello.py + +Apache +^^^^^^ + +At the top level in httpd.conf:: + + FastCgiIpcDir /tmp + FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 + +And inside the relevant VirtualHost section:: + + # FastCGI config + AddHandler fastcgi-script .fcgi + ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 + +Lighttpd +^^^^^^^^ + +For `Lighttpd `_ you can follow these +instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is +active within ``server.modules``. Then, within your ``$HTTP["host"]`` +directive, configure your fastcgi script like the following:: + + $HTTP["url"] =~ "" { + fastcgi.server = ( + "/" => ( + "script.fcgi" => ( + "bin-path" => "/path/to/your/script.fcgi", + "socket" => "/tmp/script.sock", + "check-local" => "disable", + "disable-time" => 1, + "min-procs" => 1, + "max-procs" => 1, # adjust as needed + ), + ), + ) + } # end of $HTTP["url"] =~ "^/" + +Please see `Lighttpd FastCGI Docs +`_ for +an explanation of the possible configuration options. +""" + +import os +import sys +import time +import warnings +import contextlib + +import portend + + +class Timeouts: + occupied = 5 + free = 1 + + +class ServerAdapter(object): + + """Adapter for an HTTP server. + + If you need to start more than one HTTP server (to serve on multiple + ports, or protocols, etc.), you can manually register each one and then + start them all with bus.start:: + + s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + bus.start() + """ + + def __init__(self, bus, httpserver=None, bind_addr=None): + self.bus = bus + self.httpserver = httpserver + self.bind_addr = bind_addr + self.interrupt = None + self.running = False + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + + def unsubscribe(self): + self.bus.unsubscribe('start', self.start) + self.bus.unsubscribe('stop', self.stop) + + def start(self): + """Start the HTTP server.""" + if self.running: + self.bus.log('Already serving on %s' % self.description) + return + + self.interrupt = None + if not self.httpserver: + raise ValueError('No HTTP server has been created.') + + if not os.environ.get('LISTEN_PID', None): + # Start the httpserver in a new thread. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bind_addr, timeout=Timeouts.free) + + import threading + t = threading.Thread(target=self._start_http_thread) + t.name = 'HTTPServer ' + t.name + t.start() + + self.wait() + self.running = True + self.bus.log('Serving on %s' % self.description) + start.priority = 75 + + @property + def description(self): + """ + A description about where this server is bound. + """ + if self.bind_addr is None: + on_what = 'unknown interface (dynamic?)' + elif isinstance(self.bind_addr, tuple): + on_what = self._get_base() + else: + on_what = 'socket file: %s' % self.bind_addr + return on_what + + def _get_base(self): + if not self.httpserver: + return '' + host, port = self.bound_addr + if getattr(self.httpserver, 'ssl_adapter', None): + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) + + def _start_http_thread(self): + """HTTP servers MUST be running in new threads, so that the + main thread persists to receive KeyboardInterrupt's. If an + exception is raised in the httpserver's thread then it's + trapped here, and the bus (and therefore our httpserver) + are shut down. + """ + try: + self.httpserver.start() + except KeyboardInterrupt: + self.bus.log(' hit: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + except SystemExit: + self.bus.log('SystemExit raised: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + raise + except Exception: + self.interrupt = sys.exc_info()[1] + self.bus.log('Error in HTTP server: shutting down', + traceback=True, level=40) + self.bus.exit() + raise + + def wait(self): + """Wait until the HTTP server is ready to receive requests.""" + while not getattr(self.httpserver, 'ready', False): + if self.interrupt: + raise self.interrupt + time.sleep(.1) + + # bypass check when LISTEN_PID is set + if os.environ.get('LISTEN_PID', None): + return + + # bypass check when running via socket-activation + # (for socket-activation the port will be managed by systemd) + if not isinstance(self.bind_addr, tuple): + return + + # wait for port to be occupied + with _safe_wait(*self.bound_addr): + portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) + + @property + def bound_addr(self): + """ + The bind address, or if it's an ephemeral port and the + socket has been bound, return the actual port bound. + """ + host, port = self.bind_addr + if port == 0 and self.httpserver.socket: + # Bound to ephemeral port. Get the actual port allocated. + port = self.httpserver.socket.getsockname()[1] + return host, port + + def stop(self): + """Stop the HTTP server.""" + if self.running: + # stop() MUST block until the server is *truly* stopped. + self.httpserver.stop() + # Wait for the socket to be truly freed. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bound_addr, timeout=Timeouts.free) + self.running = False + self.bus.log('HTTP Server %s shut down' % self.httpserver) + else: + self.bus.log('HTTP Server %s already shut down' % self.httpserver) + stop.priority = 25 + + def restart(self): + """Restart the HTTP server.""" + self.stop() + self.start() + + +class FlupCGIServer(object): + + """Adapter for a flup.server.cgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the CGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.cgi import WSGIServer + + self.cgiserver = WSGIServer(*self.args, **self.kwargs) + self.ready = True + self.cgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + + +class FlupFCGIServer(object): + + """Adapter for a flup.server.fcgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + if kwargs.get('bindAddress', None) is None: + import socket + if not hasattr(socket, 'fromfd'): + raise ValueError( + 'Dynamic FCGI server not available on this platform. ' + 'You must use a static or external one by providing a ' + 'legal bindAddress.') + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the FCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.fcgi import WSGIServer + self.fcgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.fcgiserver._installSignalHandlers = lambda: None + self.fcgiserver._oldSIGs = [] + self.ready = True + self.fcgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + # Forcibly stop the fcgi server main event loop. + self.fcgiserver._keepGoing = False + # Force all worker threads to die off. + self.fcgiserver._threadPool.maxSpare = ( + self.fcgiserver._threadPool._idleCount) + self.ready = False + + +class FlupSCGIServer(object): + + """Adapter for a flup.server.scgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the SCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.scgi import WSGIServer + self.scgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.scgiserver._installSignalHandlers = lambda: None + self.scgiserver._oldSIGs = [] + self.ready = True + self.scgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + # Forcibly stop the scgi server main event loop. + self.scgiserver._keepGoing = False + # Force all worker threads to die off. + self.scgiserver._threadPool.maxSpare = 0 + + +@contextlib.contextmanager +def _safe_wait(host, port): + """ + On systems where a loopback interface is not available and the + server is bound to all interfaces, it's difficult to determine + whether the server is in fact occupying the port. In this case, + just issue a warning and move on. See issue #1100. + """ + try: + yield + except portend.Timeout: + if host == portend.client_host(host): + raise + msg = 'Unable to verify that the server is bound on %r' % port + warnings.warn(msg) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/win32.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/win32.py new file mode 100644 index 000000000..b7a79b1b0 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/win32.py @@ -0,0 +1,183 @@ +"""Windows service. Requires pywin32.""" + +import os +import win32api +import win32con +import win32event +import win32service +import win32serviceutil + +from cherrypy.process import wspbus, plugins + + +class ConsoleCtrlHandler(plugins.SimplePlugin): + + """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" + + def __init__(self, bus): + self.is_set = False + plugins.SimplePlugin.__init__(self, bus) + + def start(self): + if self.is_set: + self.bus.log('Handler for console events already set.', level=20) + return + + result = win32api.SetConsoleCtrlHandler(self.handle, 1) + if result == 0: + self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Set handler for console events.', level=20) + self.is_set = True + + def stop(self): + if not self.is_set: + self.bus.log('Handler for console events already off.', level=20) + return + + try: + result = win32api.SetConsoleCtrlHandler(self.handle, 0) + except ValueError: + # "ValueError: The object has not been registered" + result = 1 + + if result == 0: + self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Removed handler for console events.', level=20) + self.is_set = False + + def handle(self, event): + """Handle console control events (like Ctrl-C).""" + if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT): + self.bus.log('Console event %s: shutting down bus' % event) + + # Remove self immediately so repeated Ctrl-C doesn't re-call it. + try: + self.stop() + except ValueError: + pass + + self.bus.exit() + # 'First to return True stops the calls' + return 1 + return 0 + + +class Win32Bus(wspbus.Bus): + + """A Web Site Process Bus implementation for Win32. + + Instead of time.sleep, this bus blocks using native win32event objects. + """ + + def __init__(self): + self.events = {} + wspbus.Bus.__init__(self) + + def _get_state_event(self, state): + """Return a win32event for the given state (creating it if needed).""" + try: + return self.events[state] + except KeyError: + event = win32event.CreateEvent(None, 0, 0, + 'WSPBus %s Event (pid=%r)' % + (state.name, os.getpid())) + self.events[state] = event + return event + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + self._state = value + event = self._get_state_event(value) + win32event.PulseEvent(event) + + def wait(self, state, interval=0.1, channel=None): + """Wait for the given state(s), KeyboardInterrupt or SystemExit. + + Since this class uses native win32event objects, the interval + argument is ignored. + """ + if isinstance(state, (tuple, list)): + # Don't wait for an event that beat us to the punch ;) + if self.state not in state: + events = tuple([self._get_state_event(s) for s in state]) + win32event.WaitForMultipleObjects( + events, 0, win32event.INFINITE) + else: + # Don't wait for an event that beat us to the punch ;) + if self.state != state: + event = self._get_state_event(state) + win32event.WaitForSingleObject(event, win32event.INFINITE) + + +class _ControlCodes(dict): + + """Control codes used to "signal" a service via ControlService. + + User-defined control codes are in the range 128-255. We generally use + the standard Python value for the Linux signal and add 128. Example: + + >>> signal.SIGUSR1 + 10 + control_codes['graceful'] = 128 + 10 + """ + + def key_for(self, obj): + """For the given value, return its corresponding key.""" + for key, val in self.items(): + if val is obj: + return key + raise ValueError('The given object could not be found: %r' % obj) + + +control_codes = _ControlCodes({'graceful': 138}) + + +def signal_child(service, command): + if command == 'stop': + win32serviceutil.StopService(service) + elif command == 'restart': + win32serviceutil.RestartService(service) + else: + win32serviceutil.ControlService(service, control_codes[command]) + + +class PyWebService(win32serviceutil.ServiceFramework): + + """Python Web Service.""" + + _svc_name_ = 'Python Web Service' + _svc_display_name_ = 'Python Web Service' + _svc_deps_ = None # sequence of service names on which this depends + _exe_name_ = 'pywebsvc' + _exe_args_ = None # Default to no arguments + + # Only exists on Windows 2000 or later, ignored on windows NT + _svc_description_ = 'Python Web Service' + + def SvcDoRun(self): + from cherrypy import process + process.bus.start() + process.bus.block() + + def SvcStop(self): + from cherrypy import process + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + process.bus.exit() + + def SvcOther(self, control): + from cherrypy import process + process.bus.publish(control_codes.key_for(control)) + + +if __name__ == '__main__': + win32serviceutil.HandleCommandLine(PyWebService) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/process/wspbus.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/wspbus.py new file mode 100644 index 000000000..a60cd51ed --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/process/wspbus.py @@ -0,0 +1,587 @@ +r"""An implementation of the Web Site Process Bus. + +This module is completely standalone, depending only on the stdlib. + +Web Site Process Bus +-------------------- + +A Bus object is used to contain and manage site-wide behavior: +daemonization, HTTP server start/stop, process reload, signal handling, +drop privileges, PID file management, logging for all of these, +and many more. + +In addition, a Bus object provides a place for each web framework +to register code that runs in response to site-wide events (like +process start and stop), or which controls or otherwise interacts with +the site-wide components mentioned above. For example, a framework which +uses file-based templates would add known template filenames to an +autoreload component. + +Ideally, a Bus object will be flexible enough to be useful in a variety +of invocation scenarios: + + 1. The deployer starts a site from the command line via a + framework-neutral deployment script; applications from multiple frameworks + are mixed in a single site. Command-line arguments and configuration + files are used to define site-wide components such as the HTTP server, + WSGI component graph, autoreload behavior, signal handling, etc. + 2. The deployer starts a site via some other process, such as Apache; + applications from multiple frameworks are mixed in a single site. + Autoreload and signal handling (from Python at least) are disabled. + 3. The deployer starts a site via a framework-specific mechanism; + for example, when running tests, exploring tutorials, or deploying + single applications from a single framework. The framework controls + which site-wide components are enabled as it sees fit. + +The Bus object in this package uses topic-based publish-subscribe +messaging to accomplish all this. A few topic channels are built in +('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and +site containers are free to define their own. If a message is sent to a +channel that has not been defined or has no listeners, there is no effect. + +In general, there should only ever be a single Bus object per process. +Frameworks and site containers share a single Bus object by publishing +messages and subscribing listeners. + +The Bus object works as a finite state machine which models the current +state of the process. Bus methods move it from one state to another; +those methods then publish to subscribed listeners on the channel for +the new state.:: + + O + | + V + STOPPING --> STOPPED --> EXITING -> X + A A | + | \___ | + | \ | + | V V + STARTED <-- STARTING + +""" + +import atexit + +try: + import ctypes +except ImportError: + """Google AppEngine is shipped without ctypes + + :seealso: http://stackoverflow.com/a/6523777/70170 + """ + ctypes = None + +import operator +import os +import sys +import threading +import time +import traceback as _traceback +import warnings +import subprocess +import functools + +from more_itertools import always_iterable + + +# Here I save the value of os.getcwd(), which, if I am imported early enough, +# will be the directory from which the startup script was run. This is needed +# by _do_execv(), to change back to the original directory before execv()ing a +# new process. This is a defense against the application having changed the +# current working directory (which could make sys.executable "not found" if +# sys.executable is a relative-path, and/or cause other problems). +_startup_cwd = os.getcwd() + + +class ChannelFailures(Exception): + """Exception raised during errors on Bus.publish().""" + + delimiter = '\n' + + def __init__(self, *args, **kwargs): + """Initialize ChannelFailures errors wrapper.""" + super(ChannelFailures, self).__init__(*args, **kwargs) + self._exceptions = list() + + def handle_exception(self): + """Append the current exception to self.""" + self._exceptions.append(sys.exc_info()[1]) + + def get_instances(self): + """Return a list of seen exception instances.""" + return self._exceptions[:] + + def __str__(self): + """Render the list of errors, which happened in channel.""" + exception_strings = map(repr, self.get_instances()) + return self.delimiter.join(exception_strings) + + __repr__ = __str__ + + def __bool__(self): + """Determine whether any error happened in channel.""" + return bool(self._exceptions) + __nonzero__ = __bool__ + +# Use a flag to indicate the state of the bus. + + +class _StateEnum(object): + + class State(object): + name = None + + def __repr__(self): + return 'states.%s' % self.name + + def __setattr__(self, key, value): + if isinstance(value, self.State): + value.name = key + object.__setattr__(self, key, value) + + +states = _StateEnum() +states.STOPPED = states.State() +states.STARTING = states.State() +states.STARTED = states.State() +states.STOPPING = states.State() +states.EXITING = states.State() + + +try: + import fcntl +except ImportError: + max_files = 0 +else: + try: + max_files = os.sysconf('SC_OPEN_MAX') + except AttributeError: + max_files = 1024 + + +class Bus(object): + """Process state-machine and messenger for HTTP site deployment. + + All listeners for a given channel are guaranteed to be called even + if others at the same channel fail. Each failure is logged, but + execution proceeds on to the next listener. The only way to stop all + processing from inside a listener is to raise SystemExit and stop the + whole server. + """ + + states = states + state = states.STOPPED + execv = False + max_cloexec_files = max_files + + def __init__(self): + """Initialize pub/sub bus.""" + self.execv = False + self.state = states.STOPPED + channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main' + self.listeners = dict( + (channel, set()) + for channel in channels + ) + self._priorities = {} + + def subscribe(self, channel, callback=None, priority=None): + """Add the given callback at the given channel (if not present). + + If callback is None, return a partial suitable for decorating + the callback. + """ + if callback is None: + return functools.partial( + self.subscribe, + channel, + priority=priority, + ) + + ch_listeners = self.listeners.setdefault(channel, set()) + ch_listeners.add(callback) + + if priority is None: + priority = getattr(callback, 'priority', 50) + self._priorities[(channel, callback)] = priority + + def unsubscribe(self, channel, callback): + """Discard the given callback (if present).""" + listeners = self.listeners.get(channel) + if listeners and callback in listeners: + listeners.discard(callback) + del self._priorities[(channel, callback)] + + def publish(self, channel, *args, **kwargs): + """Return output of all subscribers for the given channel.""" + if channel not in self.listeners: + return [] + + exc = ChannelFailures() + output = [] + + raw_items = ( + (self._priorities[(channel, listener)], listener) + for listener in self.listeners[channel] + ) + items = sorted(raw_items, key=operator.itemgetter(0)) + for priority, listener in items: + try: + output.append(listener(*args, **kwargs)) + except KeyboardInterrupt: + raise + except SystemExit: + e = sys.exc_info()[1] + # If we have previous errors ensure the exit code is non-zero + if exc and e.code == 0: + e.code = 1 + raise + except Exception: + exc.handle_exception() + if channel == 'log': + # Assume any further messages to 'log' will fail. + pass + else: + self.log('Error in %r listener %r' % (channel, listener), + level=40, traceback=True) + if exc: + raise exc + return output + + def _clean_exit(self): + """Assert that the Bus is not running in atexit handler callback.""" + if self.state != states.EXITING: + warnings.warn( + 'The main thread is exiting, but the Bus is in the %r state; ' + 'shutting it down automatically now. You must either call ' + 'bus.block() after start(), or call bus.exit() before the ' + 'main thread exits.' % self.state, RuntimeWarning) + self.exit() + + def start(self): + """Start all services.""" + atexit.register(self._clean_exit) + + self.state = states.STARTING + self.log('Bus STARTING') + try: + self.publish('start') + self.state = states.STARTED + self.log('Bus STARTED') + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.log('Shutting down due to error in start listener:', + level=40, traceback=True) + e_info = sys.exc_info()[1] + try: + self.exit() + except Exception: + # Any stop/exit errors will be logged inside publish(). + pass + # Re-raise the original error + raise e_info + + def exit(self): + """Stop all services and prepare to exit the process.""" + exitstate = self.state + EX_SOFTWARE = 70 + try: + self.stop() + + self.state = states.EXITING + self.log('Bus EXITING') + self.publish('exit') + # This isn't strictly necessary, but it's better than seeing + # "Waiting for child threads to terminate..." and then nothing. + self.log('Bus EXITED') + except Exception: + # This method is often called asynchronously (whether thread, + # signal handler, console handler, or atexit handler), so we + # can't just let exceptions propagate out unhandled. + # Assume it's been logged and just die. + os._exit(EX_SOFTWARE) + + if exitstate == states.STARTING: + # exit() was called before start() finished, possibly due to + # Ctrl-C because a start listener got stuck. In this case, + # we could get stuck in a loop where Ctrl-C never exits the + # process, so we just call os.exit here. + os._exit(EX_SOFTWARE) + + def restart(self): + """Restart the process (may close connections). + + This method does not restart the process from the calling thread; + instead, it stops the bus and asks the main thread to call execv. + """ + self.execv = True + self.exit() + + def graceful(self): + """Advise all services to reload.""" + self.log('Bus graceful') + self.publish('graceful') + + def block(self, interval=0.1): + """Wait for the EXITING state, KeyboardInterrupt or SystemExit. + + This function is intended to be called only by the main thread. + After waiting for the EXITING state, it also waits for all threads + to terminate, and then calls os.execv if self.execv is True. This + design allows another thread to call bus.restart, yet have the main + thread perform the actual execv call (required on some platforms). + """ + try: + self.wait(states.EXITING, interval=interval, channel='main') + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.log('Keyboard Interrupt: shutting down bus') + self.exit() + except SystemExit: + self.log('SystemExit raised: shutting down bus') + self.exit() + raise + + # Waiting for ALL child threads to finish is necessary on OS X. + # See https://github.com/cherrypy/cherrypy/issues/581. + # It's also good to let them all shut down before allowing + # the main thread to call atexit handlers. + # See https://github.com/cherrypy/cherrypy/issues/751. + self.log('Waiting for child threads to terminate...') + for t in threading.enumerate(): + # Validate the we're not trying to join the MainThread + # that will cause a deadlock and the case exist when + # implemented as a windows service and in any other case + # that another thread executes cherrypy.engine.exit() + if ( + t != threading.current_thread() and + not isinstance(t, threading._MainThread) and + # Note that any dummy (external) threads are + # always daemonic. + not t.daemon + ): + self.log('Waiting for thread %s.' % t.name) + t.join() + + if self.execv: + self._do_execv() + + def wait(self, state, interval=0.1, channel=None): + """Poll for the given state(s) at intervals; publish to channel.""" + states = set(always_iterable(state)) + + while self.state not in states: + time.sleep(interval) + self.publish(channel) + + def _do_execv(self): + """Re-execute the current process. + + This must be called from the main thread, because certain platforms + (OS X) don't allow execv to be called in a child thread very well. + """ + try: + args = self._get_true_argv() + except NotImplementedError: + """It's probably win32 or GAE""" + args = [sys.executable] + self._get_interpreter_argv() + sys.argv + + self.log('Re-spawning %s' % ' '.join(args)) + + self._extend_pythonpath(os.environ) + + if sys.platform[:4] == 'java': + from _systemrestart import SystemRestart + raise SystemRestart + else: + if sys.platform == 'win32': + args = ['"%s"' % arg for arg in args] + + os.chdir(_startup_cwd) + if self.max_cloexec_files: + self._set_cloexec() + os.execv(sys.executable, args) + + @staticmethod + def _get_interpreter_argv(): + """Retrieve current Python interpreter's arguments. + + Returns empty tuple in case of frozen mode, uses built-in arguments + reproduction function otherwise. + + Frozen mode is possible for the app has been packaged into a binary + executable using py2exe. In this case the interpreter's arguments are + already built-in into that executable. + + :seealso: https://github.com/cherrypy/cherrypy/issues/1526 + Ref: https://pythonhosted.org/PyInstaller/runtime-information.html + """ + return ([] + if getattr(sys, 'frozen', False) + else subprocess._args_from_interpreter_flags()) + + @staticmethod + def _get_true_argv(): + """Retrieve all real arguments of the python interpreter. + + ...even those not listed in ``sys.argv`` + + :seealso: http://stackoverflow.com/a/28338254/595220 + :seealso: http://stackoverflow.com/a/6683222/595220 + :seealso: http://stackoverflow.com/a/28414807/595220 + """ + try: + char_p = ctypes.c_wchar_p + + argv = ctypes.POINTER(char_p)() + argc = ctypes.c_int() + + ctypes.pythonapi.Py_GetArgcArgv( + ctypes.byref(argc), + ctypes.byref(argv), + ) + + _argv = argv[:argc.value] + + # The code below is trying to correctly handle special cases. + # `-c`'s argument interpreted by Python itself becomes `-c` as + # well. Same applies to `-m`. This snippet is trying to survive + # at least the case with `-m` + # Ref: https://github.com/cherrypy/cherrypy/issues/1545 + # Ref: python/cpython@418baf9 + argv_len, is_command, is_module = len(_argv), False, False + + try: + m_ind = _argv.index('-m') + if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'): + """ + In some older Python versions `-m`'s argument may be + substituted with `-c`, not `-m` + """ + is_module = True + except (IndexError, ValueError): + m_ind = None + + try: + c_ind = _argv.index('-c') + if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c': + is_command = True + except (IndexError, ValueError): + c_ind = None + + if is_module: + """It's containing `-m -m` sequence of arguments""" + if is_command and c_ind < m_ind: + """There's `-c -c` before `-m`""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. Ref: " + 'https://github.com/cherrypy/cherrypy/issues/1545') + # Survive module argument here + original_module = sys.argv[0] + if not os.access(original_module, os.R_OK): + """There's no such module exist""" + raise AttributeError( + "{} doesn't seem to be a module " + 'accessible by current user'.format(original_module)) + del _argv[m_ind:m_ind + 2] # remove `-m -m` + # ... and substitute it with the original module path: + _argv.insert(m_ind, original_module) + elif is_command: + """It's containing just `-c -c` sequence of arguments""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. " + 'Ref: https://github.com/cherrypy/cherrypy/issues/1545') + except AttributeError: + """It looks Py_GetArgcArgv's completely absent in some environments + + It is known, that there's no Py_GetArgcArgv in MS Windows and + ``ctypes`` module is completely absent in Google AppEngine + + :seealso: https://github.com/cherrypy/cherrypy/issues/1506 + :seealso: https://github.com/cherrypy/cherrypy/issues/1512 + :ref: http://bit.ly/2gK6bXK + """ + raise NotImplementedError + else: + return _argv + + @staticmethod + def _extend_pythonpath(env): + """Prepend current working dir to PATH environment variable if needed. + + If sys.path[0] is an empty string, the interpreter was likely + invoked with -m and the effective path is about to change on + re-exec. Add the current directory to $PYTHONPATH to ensure + that the new process sees the same path. + + This issue cannot be addressed in the general case because + Python cannot reliably reconstruct the + original command line (http://bugs.python.org/issue14208). + + (This idea filched from tornado.autoreload) + """ + path_prefix = '.' + os.pathsep + existing_path = env.get('PYTHONPATH', '') + needs_patch = ( + sys.path[0] == '' and + not existing_path.startswith(path_prefix) + ) + + if needs_patch: + env['PYTHONPATH'] = path_prefix + existing_path + + def _set_cloexec(self): + """Set the CLOEXEC flag on all open files (except stdin/out/err). + + If self.max_cloexec_files is an integer (the default), then on + platforms which support it, it represents the max open files setting + for the operating system. This function will be called just before + the process is restarted via os.execv() to prevent open files + from persisting into the new process. + + Set self.max_cloexec_files to 0 to disable this behavior. + """ + for fd in range(3, self.max_cloexec_files): # skip stdin/out/err + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + except IOError: + continue + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + def stop(self): + """Stop all services.""" + self.state = states.STOPPING + self.log('Bus STOPPING') + self.publish('stop') + self.state = states.STOPPED + self.log('Bus STOPPED') + + def start_with_callback(self, func, args=None, kwargs=None): + """Start 'func' in a new thread T, then start self (and return T).""" + if args is None: + args = () + if kwargs is None: + kwargs = {} + args = (func,) + args + + def _callback(func, *a, **kw): + self.wait(states.STARTED) + func(*a, **kw) + t = threading.Thread(target=_callback, args=args, kwargs=kwargs) + t.name = 'Bus Callback ' + t.name + t.start() + + self.start() + + return t + + def log(self, msg='', level=20, traceback=False): + """Log the given message. Append the last traceback if requested.""" + if traceback: + msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) + self.publish('log', msg, level) + + +bus = Bus() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__init__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__init__.py new file mode 100644 index 000000000..bcddba2db --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__init__.py @@ -0,0 +1,63 @@ +""", a CherryPy application. + +Use this as a base for creating new CherryPy applications. When you want +to make a new app, copy and paste this folder to some other location +(maybe site-packages) and rename it to the name of your project, +then tweak as desired. + +Even before any tweaking, this should serve a few demonstration pages. +Change to this directory and run: + + cherryd -c site.conf + +""" + +import cherrypy +from cherrypy import tools, url + +import os +local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +@cherrypy.config(**{'tools.log_tracebacks.on': True}) +class Root: + """Declaration of the CherryPy app URI structure.""" + + @cherrypy.expose + def index(self): + """Render HTML-template at the root path of the web-app.""" + return """ +Try some other path, +or a default path.
+Or, just look at the pretty picture:
+ +""" % (url('other'), url('else'), + url('files/made_with_cherrypy_small.png')) + + @cherrypy.expose + def default(self, *args, **kwargs): + """Render catch-all args and kwargs.""" + return 'args: %s kwargs: %s' % (args, kwargs) + + @cherrypy.expose + def other(self, a=2, b='bananas', c=None): + """Render number of fruits based on third argument.""" + cherrypy.response.headers['Content-Type'] = 'text/plain' + if c is None: + return 'Have %d %s.' % (int(a), b) + else: + return 'Have %d %s, %s.' % (int(a), b, c) + + files = tools.staticdir.handler( + section='/files', + dir=os.path.join(local_dir, 'static'), + # Ignore .php files, etc. + match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', + ) + + +root = Root() + +# Uncomment the following to use your own favicon instead of CP's default. +# favicon_path = os.path.join(local_dir, "favicon.ico") +# root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..e418a7463 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/apache-fcgi.conf b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/apache-fcgi.conf new file mode 100644 index 000000000..6e4f144cf --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/apache-fcgi.conf @@ -0,0 +1,22 @@ +# Apache2 server conf file for using CherryPy with mod_fcgid. + +# This doesn't have to be "C:/", but it has to be a directory somewhere, and +# MUST match the directory used in the FastCgiExternalServer directive, below. +DocumentRoot "C:/" + +ServerName 127.0.0.1 +Listen 80 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +# Send requests for any URI to our fastcgi handler. +RewriteRule ^(.*)$ /fastcgi.pyc [L] + +# The FastCgiExternalServer directive defines filename as an external FastCGI application. +# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. +# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this +# filename will be handled by this external FastCGI application. +FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/example.conf b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/example.conf new file mode 100644 index 000000000..63250fe3a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/example.conf @@ -0,0 +1,3 @@ +[/] +log.error_file: "error.log" +log.access_file: "access.log" diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/site.conf b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/site.conf new file mode 100644 index 000000000..6ed389837 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/site.conf @@ -0,0 +1,14 @@ +[global] +# Uncomment this when you're done developing +#environment: "production" + +server.socket_host: "0.0.0.0" +server.socket_port: 8088 + +# Uncomment the following lines to run on HTTPS at the same time +#server.2.socket_host: "0.0.0.0" +#server.2.socket_port: 8433 +#server.2.ssl_certificate: '../test/test.pem' +#server.2.ssl_private_key: '../test/test.pem' + +tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/static/made_with_cherrypy_small.png b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/static/made_with_cherrypy_small.png new file mode 100644 index 000000000..724f9d72d Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/scaffold/static/made_with_cherrypy_small.png differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__init__.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__init__.py new file mode 100644 index 000000000..068382be1 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__init__.py @@ -0,0 +1,24 @@ +""" +Regression test suite for CherryPy. +""" + +import os +import sys + + +def newexit(): + os._exit(1) + + +def setup(): + # We want to monkey patch sys.exit so that we can get some + # information about where exit is being called. + newexit._old = sys.exit + sys.exit = newexit + + +def teardown(): + try: + sys.exit = sys.exit._old + except AttributeError: + sys.exit = sys._exit diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/__init__.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..e3dbfc1d3 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/__init__.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_decorators.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_decorators.cpython-310.pyc new file mode 100644 index 000000000..7b1291bdf Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_decorators.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_states_demo.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_states_demo.cpython-310.pyc new file mode 100644 index 000000000..45bc2715b Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/_test_states_demo.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/benchmark.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/benchmark.cpython-310.pyc new file mode 100644 index 000000000..e6b2b244b Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/benchmark.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/checkerdemo.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/checkerdemo.cpython-310.pyc new file mode 100644 index 000000000..317eeefa9 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/checkerdemo.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/helper.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/helper.cpython-310.pyc new file mode 100644 index 000000000..9391706c2 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/helper.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/logtest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/logtest.cpython-310.pyc new file mode 100644 index 000000000..010e13ab7 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/logtest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfastcgi.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfastcgi.cpython-310.pyc new file mode 100644 index 000000000..4ab11a8a0 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfastcgi.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfcgid.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfcgid.cpython-310.pyc new file mode 100644 index 000000000..597fe06ab Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modfcgid.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modpy.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modpy.cpython-310.pyc new file mode 100644 index 000000000..ba4b36f1a Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modpy.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modwsgi.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modwsgi.cpython-310.pyc new file mode 100644 index 000000000..e3e51eeb1 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/modwsgi.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/sessiondemo.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/sessiondemo.cpython-310.pyc new file mode 100644 index 000000000..5ab806390 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/sessiondemo.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_basic.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_basic.cpython-310.pyc new file mode 100644 index 000000000..a3c2ea495 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_basic.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_digest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_digest.cpython-310.pyc new file mode 100644 index 000000000..ec2abc563 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_auth_digest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_bus.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_bus.cpython-310.pyc new file mode 100644 index 000000000..e3a3091cc Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_bus.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_caching.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_caching.cpython-310.pyc new file mode 100644 index 000000000..18a31d9ad Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_caching.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config.cpython-310.pyc new file mode 100644 index 000000000..8e3469190 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config_server.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config_server.cpython-310.pyc new file mode 100644 index 000000000..f08906259 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_config_server.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_conn.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_conn.cpython-310.pyc new file mode 100644 index 000000000..acc3b27c9 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_conn.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_core.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_core.cpython-310.pyc new file mode 100644 index 000000000..5dec88022 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_core.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_dynamicobjectmapping.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_dynamicobjectmapping.cpython-310.pyc new file mode 100644 index 000000000..4f8ccdfcf Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_dynamicobjectmapping.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_encoding.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_encoding.cpython-310.pyc new file mode 100644 index 000000000..af39dbd2a Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_encoding.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_etags.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_etags.cpython-310.pyc new file mode 100644 index 000000000..a617cbabe Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_etags.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_http.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_http.cpython-310.pyc new file mode 100644 index 000000000..7ff78c430 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_http.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_httputil.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_httputil.cpython-310.pyc new file mode 100644 index 000000000..7dedb0320 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_httputil.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_iterator.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_iterator.cpython-310.pyc new file mode 100644 index 000000000..68bff30bf Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_iterator.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_json.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_json.cpython-310.pyc new file mode 100644 index 000000000..20e323b63 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_json.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_logging.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_logging.cpython-310.pyc new file mode 100644 index 000000000..718eb7f2f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_logging.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_mime.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_mime.cpython-310.pyc new file mode 100644 index 000000000..f69d65b06 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_mime.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_misc_tools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_misc_tools.cpython-310.pyc new file mode 100644 index 000000000..bf17fbf1b Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_misc_tools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_native.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_native.cpython-310.pyc new file mode 100644 index 000000000..23b91ba76 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_native.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_objectmapping.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_objectmapping.cpython-310.pyc new file mode 100644 index 000000000..42be90786 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_objectmapping.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_params.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_params.cpython-310.pyc new file mode 100644 index 000000000..6dcb9e210 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_params.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_plugins.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_plugins.cpython-310.pyc new file mode 100644 index 000000000..6d4ff78b4 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_plugins.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_proxy.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_proxy.cpython-310.pyc new file mode 100644 index 000000000..d7e8947c1 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_proxy.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_refleaks.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_refleaks.cpython-310.pyc new file mode 100644 index 000000000..34d4c48c1 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_refleaks.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_request_obj.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_request_obj.cpython-310.pyc new file mode 100644 index 000000000..468032cc0 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_request_obj.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_routes.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_routes.cpython-310.pyc new file mode 100644 index 000000000..071d01d82 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_routes.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_session.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_session.cpython-310.pyc new file mode 100644 index 000000000..00ca33fdc Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_session.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_sessionauthenticate.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_sessionauthenticate.cpython-310.pyc new file mode 100644 index 000000000..f1da90bb5 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_sessionauthenticate.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_states.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_states.cpython-310.pyc new file mode 100644 index 000000000..ac2bdc7e9 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_states.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_static.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_static.cpython-310.pyc new file mode 100644 index 000000000..7a54febbb Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_static.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tools.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tools.cpython-310.pyc new file mode 100644 index 000000000..9378692b4 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tools.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tutorials.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tutorials.cpython-310.pyc new file mode 100644 index 000000000..ab638cf8c Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_tutorials.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_virtualhost.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_virtualhost.cpython-310.pyc new file mode 100644 index 000000000..9d9c30867 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_virtualhost.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_ns.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_ns.cpython-310.pyc new file mode 100644 index 000000000..e84ede359 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_ns.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_unix_socket.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_unix_socket.cpython-310.pyc new file mode 100644 index 000000000..df72a9ef8 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_unix_socket.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_vhost.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_vhost.cpython-310.pyc new file mode 100644 index 000000000..8dba6945f Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgi_vhost.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgiapps.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgiapps.cpython-310.pyc new file mode 100644 index 000000000..2f662b511 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_wsgiapps.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_xmlrpc.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_xmlrpc.cpython-310.pyc new file mode 100644 index 000000000..b9d555628 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/test_xmlrpc.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/webtest.cpython-310.pyc b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/webtest.cpython-310.pyc new file mode 100644 index 000000000..1c7297816 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/__pycache__/webtest.cpython-310.pyc differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_decorators.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_decorators.py new file mode 100644 index 000000000..74832e40a --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_decorators.py @@ -0,0 +1,39 @@ +"""Test module for the @-decorator syntax, which is version-specific""" + +import cherrypy +from cherrypy import expose, tools + + +class ExposeExamples(object): + + @expose + def no_call(self): + return 'Mr E. R. Bradshaw' + + @expose() + def call_empty(self): + return 'Mrs. B.J. Smegma' + + @expose('call_alias') + def nesbitt(self): + return 'Mr Nesbitt' + + @expose(['alias1', 'alias2']) + def andrews(self): + return 'Mr Ken Andrews' + + @expose(alias='alias3') + def watson(self): + return 'Mr. and Mrs. Watson' + + +class ToolExamples(object): + + @expose + # This is here to demonstrate that using the config decorator + # does not overwrite other config attributes added by the Tool + # decorator (in this case response_headers). + @cherrypy.config(**{'response.stream': True}) + @tools.response_headers(headers=[('Content-Type', 'application/data')]) + def blah(self): + yield b'blah' diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_states_demo.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_states_demo.py new file mode 100644 index 000000000..a49407baf --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/_test_states_demo.py @@ -0,0 +1,69 @@ +import os +import sys +import time + +import cherrypy + +starttime = time.time() + + +class Root: + + @cherrypy.expose + def index(self): + return 'Hello World' + + @cherrypy.expose + def mtimes(self): + return repr(cherrypy.engine.publish('Autoreloader', 'mtimes')) + + @cherrypy.expose + def pid(self): + return str(os.getpid()) + + @cherrypy.expose + def start(self): + return repr(starttime) + + @cherrypy.expose + def exit(self): + # This handler might be called before the engine is STARTED if an + # HTTP worker thread handles it before the HTTP server returns + # control to engine.start. We avoid that race condition here + # by waiting for the Bus to be STARTED. + cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) + cherrypy.engine.exit() + + +@cherrypy.engine.subscribe('start', priority=100) +def unsub_sig(): + cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False)) + if cherrypy.config.get('unsubsig', False): + cherrypy.log('Unsubscribing the default cherrypy signal handler') + cherrypy.engine.signal_handler.unsubscribe() + try: + from signal import signal, SIGTERM + except ImportError: + pass + else: + def old_term_handler(signum=None, frame=None): + cherrypy.log('I am an old SIGTERM handler.') + sys.exit(0) + cherrypy.log('Subscribing the new one.') + signal(SIGTERM, old_term_handler) + + +@cherrypy.engine.subscribe('start', priority=6) +def starterror(): + if cherrypy.config.get('starterror', False): + 1 / 0 + + +@cherrypy.engine.subscribe('start', priority=6) +def log_test_case_name(): + if cherrypy.config.get('test_case_name', False): + cherrypy.log('STARTED FROM: %s' % + cherrypy.config.get('test_case_name')) + + +cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/benchmark.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/benchmark.py new file mode 100644 index 000000000..44dfeff12 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/benchmark.py @@ -0,0 +1,425 @@ +"""CherryPy Benchmark Tool + + Usage: + benchmark.py [options] + + --null: use a null Request object (to bench the HTTP server only) + --notests: start the server but do not run the tests; this allows + you to check the tested pages with a browser + --help: show this help message + --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) + --modpython: run tests via apache on 54583 (with modpython_gateway) + --ab=path: Use the ab script/executable at 'path' (see below) + --apache=path: Use the apache script/exe at 'path' (see below) + + To run the benchmarks, the Apache Benchmark tool "ab" must either be on + your system path, or specified via the --ab=path option. + + To run the modpython tests, the "apache" executable or script must be + on your system path, or provided via the --apache=path option. On some + platforms, "apache" may be called "apachectl" or "apache2ctl"--create + a symlink to them if needed. +""" + +import getopt +import os +import re +import sys +import time + +import cherrypy +from cherrypy import _cperror, _cpmodpy +from cherrypy.lib import httputil + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +AB_PATH = '' +APACHE_PATH = 'apache' +SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' + +__all__ = ['ABSession', 'Root', 'print_report', + 'run_standard_benchmarks', 'safe_threads', + 'size_report', 'thread_report', + ] + +size_cache = {} + + +class Root: + + @cherrypy.expose + def index(self): + return """ + + CherryPy Benchmark + + + + +""" + + @cherrypy.expose + def hello(self): + return 'Hello, world\r\n' + + @cherrypy.expose + def sizer(self, size): + resp = size_cache.get(size, None) + if resp is None: + size_cache[size] = resp = 'X' * int(size) + return resp + + +def init(): + + cherrypy.config.update({ + 'log.error.file': '', + 'environment': 'production', + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 54583, + 'server.max_request_header_size': 0, + 'server.max_request_body_size': 0, + }) + + # Cheat mode on ;) + del cherrypy.config['tools.log_tracebacks.on'] + del cherrypy.config['tools.log_headers.on'] + del cherrypy.config['tools.trailing_slash.on'] + + appconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + } + globals().update( + app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), + ) + + +class NullRequest: + + """A null HTTP request class, returning 200 and an empty body.""" + + def __init__(self, local, remote, scheme='http'): + pass + + def close(self): + pass + + def run(self, method, path, query_string, protocol, headers, rfile): + cherrypy.response.status = '200 OK' + cherrypy.response.header_list = [('Content-Type', 'text/html'), + ('Server', 'Null CherryPy'), + ('Date', httputil.HTTPDate()), + ('Content-Length', '0'), + ] + cherrypy.response.body = [''] + return cherrypy.response + + +class NullResponse: + pass + + +class ABSession: + + """A session of 'ab', the Apache HTTP server benchmarking tool. + +Example output from ab: + +This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 +Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100 requests +Completed 200 requests +Completed 300 requests +Completed 400 requests +Completed 500 requests +Completed 600 requests +Completed 700 requests +Completed 800 requests +Completed 900 requests + + +Server Software: CherryPy/3.1beta +Server Hostname: 127.0.0.1 +Server Port: 54583 + +Document Path: /static/index.html +Document Length: 14 bytes + +Concurrency Level: 10 +Time taken for tests: 9.643867 seconds +Complete requests: 1000 +Failed requests: 0 +Write errors: 0 +Total transferred: 189000 bytes +HTML transferred: 14000 bytes +Requests per second: 103.69 [#/sec] (mean) +Time per request: 96.439 [ms] (mean) +Time per request: 9.644 [ms] (mean, across all concurrent requests) +Transfer rate: 19.08 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 2.9 0 10 +Processing: 20 94 7.3 90 130 +Waiting: 0 43 28.1 40 100 +Total: 20 95 7.3 100 130 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 100 + 75% 100 + 80% 100 + 90% 100 + 95% 100 + 98% 100 + 99% 110 + 100% 130 (longest request) +Finished 1000 requests +""" + + parse_patterns = [ + ('complete_requests', 'Completed', + br'^Complete requests:\s*(\d+)'), + ('failed_requests', 'Failed', + br'^Failed requests:\s*(\d+)'), + ('requests_per_second', 'req/sec', + br'^Requests per second:\s*([0-9.]+)'), + ('time_per_request_concurrent', 'msec/req', + br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), + ('transfer_rate', 'KB/sec', + br'^Transfer rate:\s*([0-9.]+)') + ] + + def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, + concurrency=10): + self.path = path + self.requests = requests + self.concurrency = concurrency + + def args(self): + port = cherrypy.server.socket_port + assert self.concurrency > 0 + assert self.requests > 0 + # Don't use "localhost". + # Cf + # http://mail.python.org/pipermail/python-win32/2008-March/007050.html + return ('-k -n %s -c %s http://127.0.0.1:%s%s' % + (self.requests, self.concurrency, port, self.path)) + + def run(self): + # Parse output of ab, setting attributes on self + try: + self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) + except Exception: + print(_cperror.format_exc()) + raise + + for attr, name, pattern in self.parse_patterns: + val = re.search(pattern, self.output, re.MULTILINE) + if val: + val = val.group(1) + setattr(self, attr, val) + else: + setattr(self, attr, None) + + +safe_threads = (25, 50, 100, 200, 400) +if sys.platform in ('win32',): + # For some reason, ab crashes with > 50 threads on my Win2k laptop. + safe_threads = (10, 20, 30, 40, 50) + + +def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): + sess = ABSession(path) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + avg = dict.fromkeys(attrs, 0.0) + + yield ('threads',) + names + for c in concurrency: + sess.concurrency = c + sess.run() + row = [c] + for attr in attrs: + val = getattr(sess, attr) + if val is None: + print(sess.output) + row = None + break + val = float(val) + avg[attr] += float(val) + row.append(val) + if row: + yield row + + # Add a row of averages. + yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] + + +def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), + concurrency=50): + sess = ABSession(concurrency=concurrency) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + yield ('bytes',) + names + for sz in sizes: + sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) + sess.run() + yield [sz] + [getattr(sess, attr) for attr in attrs] + + +def print_report(rows): + for row in rows: + print('') + for val in row: + sys.stdout.write(str(val).rjust(10) + ' | ') + print('') + + +def run_standard_benchmarks(): + print('') + print('Client Thread Report (1000 requests, 14 byte response body, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report()) + + print('') + print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) + + print('') + print('Size Report (1000 requests, 50 client threads, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(size_report()) + + +# modpython and other WSGI # + +def startup_modpython(req=None): + """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). + """ + if cherrypy.engine.state == cherrypy._cpengine.STOPPED: + if req: + if 'nullreq' in req.get_options(): + cherrypy.engine.request_class = NullRequest + cherrypy.engine.response_class = NullResponse + ab_opt = req.get_options().get('ab', '') + if ab_opt: + global AB_PATH + AB_PATH = ab_opt + cherrypy.engine.start() + if cherrypy.engine.state == cherrypy._cpengine.STARTING: + cherrypy.engine.wait() + return 0 # apache.OK + + +def run_modpython(use_wsgi=False): + print('Starting mod_python...') + pyopts = [] + + # Pass the null and ab=path options through Apache + if '--null' in opts: + pyopts.append(('nullreq', '')) + + if '--ab' in opts: + pyopts.append(('ab', opts['--ab'])) + + s = _cpmodpy.ModPythonServer + if use_wsgi: + pyopts.append(('wsgi.application', 'cherrypy::tree')) + pyopts.append( + ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) + handler = 'modpython_gateway::handler' + s = s(port=54583, opts=pyopts, + apache_path=APACHE_PATH, handler=handler) + else: + pyopts.append( + ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) + + try: + s.start() + run() + finally: + s.stop() + + +if __name__ == '__main__': + init() + + longopts = ['cpmodpy', 'modpython', 'null', 'notests', + 'help', 'ab=', 'apache='] + try: + switches, args = getopt.getopt(sys.argv[1:], '', longopts) + opts = dict(switches) + except getopt.GetoptError: + print(__doc__) + sys.exit(2) + + if '--help' in opts: + print(__doc__) + sys.exit(0) + + if '--ab' in opts: + AB_PATH = opts['--ab'] + + if '--notests' in opts: + # Return without stopping the server, so that the pages + # can be tested from a standard web browser. + def run(): + port = cherrypy.server.socket_port + print('You may now open http://127.0.0.1:%s%s/' % + (port, SCRIPT_NAME)) + + if '--null' in opts: + print('Using null Request object') + else: + def run(): + end = time.time() - start + print('Started in %s seconds' % end) + if '--null' in opts: + print('\nUsing null Request object') + try: + try: + run_standard_benchmarks() + except Exception: + print(_cperror.format_exc()) + raise + finally: + cherrypy.engine.exit() + + print('Starting CherryPy app server...') + + class NullWriter(object): + + """Suppresses the printing of socket errors.""" + + def write(self, data): + pass + sys.stderr = NullWriter() + + start = time.time() + + if '--cpmodpy' in opts: + run_modpython() + elif '--modpython' in opts: + run_modpython(use_wsgi=True) + else: + if '--null' in opts: + cherrypy.server.request_class = NullRequest + cherrypy.server.response_class = NullResponse + + cherrypy.engine.start_with_callback(run) + cherrypy.engine.block() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/checkerdemo.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/checkerdemo.py new file mode 100644 index 000000000..3438bd0c5 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/checkerdemo.py @@ -0,0 +1,49 @@ +"""Demonstration app for cherrypy.checker. + +This application is intentionally broken and badly designed. +To demonstrate the output of the CherryPy Checker, simply execute +this module. +""" + +import os +import cherrypy +thisdir = os.path.dirname(os.path.abspath(__file__)) + + +class Root: + pass + + +if __name__ == '__main__': + conf = {'/base': {'tools.staticdir.root': thisdir, + # Obsolete key. + 'throw_errors': True, + }, + # This entry should be OK. + '/base/static': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on missing folder. + '/base/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'js'}, + # Warn on dir with an abs path even though we provide root. + '/base/static2': {'tools.staticdir.on': True, + 'tools.staticdir.dir': '/static'}, + # Warn on dir with a relative path with no root. + '/static3': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on unknown namespace + '/unknown': {'toobles.gzip.on': True}, + # Warn special on cherrypy..* + '/cpknown': {'cherrypy.tools.encode.on': True}, + # Warn on mismatched types + '/conftype': {'request.show_tracebacks': 14}, + # Warn on unknown tool. + '/web': {'tools.unknown.on': True}, + # Warn on server.* in app config. + '/app1': {'server.socket_host': '0.0.0.0'}, + # Warn on 'localhost' + 'global': {'server.socket_host': 'localhost'}, + # Warn on '[name]' + '[/extra_brackets]': {}, + } + cherrypy.quickstart(Root(), config=conf) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fastcgi.conf b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fastcgi.conf new file mode 100644 index 000000000..e5c5163ca --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fastcgi.conf @@ -0,0 +1,18 @@ + +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log + +DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fcgi.conf b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fcgi.conf new file mode 100644 index 000000000..3062eb35b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/fcgi.conf @@ -0,0 +1,14 @@ + +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/helper.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/helper.py new file mode 100644 index 000000000..cae495336 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/helper.py @@ -0,0 +1,520 @@ +"""A library of helper functions for the CherryPy test suite.""" + +import datetime +import io +import logging +import os +import re +import subprocess +import sys +import time +import unittest +import warnings +import contextlib + +import portend +import pytest + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob +from cherrypy.lib import httputil +from cherrypy.lib import gctools + +log = logging.getLogger(__name__) +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + + +class Supervisor(object): + + """Base class for modeling and controlling servers during testing.""" + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if k == 'port': + setattr(self, k, int(v)) + setattr(self, k, v) + + +def log_to_stderr(msg, level): + return sys.stderr.write(msg + os.linesep) + + +class LocalSupervisor(Supervisor): + + """Base class for modeling/controlling servers which run in the same + process. + + When the server side runs in a different process, start/stop can dump all + state between each test module easily. When the server side runs in the + same process as the client, however, we have to do a bit more work to + ensure config and mounted apps are reset between tests. + """ + + using_apache = False + using_wsgi = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + cherrypy.server.httpserver = self.httpserver_class + + # This is perhaps the wrong place for this call but this is the only + # place that i've found so far that I KNOW is early enough to set this. + cherrypy.config.update({'log.screen': False}) + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + def start(self, modulename=None): + """Load and start the HTTP server.""" + if modulename: + # Unhook httpserver so cherrypy.server.start() creates a new + # one (with config from setup_server, if declared). + cherrypy.server.httpserver = None + + cherrypy.engine.start() + + self.sync_apps() + + def sync_apps(self): + """Tell the server about any apps which the setup functions mounted.""" + pass + + def stop(self): + td = getattr(self, 'teardown', None) + if td: + td() + + cherrypy.engine.exit() + + for name, server in getattr(cherrypy, 'servers', {}).copy().items(): + server.unsubscribe() + del cherrypy.servers[name] + + +class NativeServerSupervisor(LocalSupervisor): + + """Server supervisor for the builtin HTTP server.""" + + httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' + using_apache = False + using_wsgi = False + + def __str__(self): + return 'Builtin HTTP Server on %s:%s' % (self.host, self.port) + + +class LocalWSGISupervisor(LocalSupervisor): + + """Server supervisor for the builtin WSGI server.""" + + httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' + using_apache = False + using_wsgi = True + + def __str__(self): + return 'Builtin WSGI Server on %s:%s' % (self.host, self.port) + + def sync_apps(self): + """Hook a new WSGI app into the origin server.""" + cherrypy.server.httpserver.wsgi_app = self.get_app() + + def get_app(self, app=None): + """Obtain a new (decorated) WSGI app to hook into the origin server.""" + if app is None: + app = cherrypy.tree + + if self.validate: + try: + from wsgiref import validate + except ImportError: + warnings.warn( + 'Error importing wsgiref. The validator will not run.') + else: + # wraps the app in the validator + app = validate.validator(app) + + return app + + +def get_cpmodpy_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_cpmodpy + return sup + + +def get_modpygw_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_modpython_gateway + sup.using_wsgi = True + return sup + + +def get_modwsgi_supervisor(**options): + from cherrypy.test import modwsgi + return modwsgi.ModWSGISupervisor(**options) + + +def get_modfcgid_supervisor(**options): + from cherrypy.test import modfcgid + return modfcgid.ModFCGISupervisor(**options) + + +def get_modfastcgi_supervisor(**options): + from cherrypy.test import modfastcgi + return modfastcgi.ModFCGISupervisor(**options) + + +def get_wsgi_u_supervisor(**options): + cherrypy.server.wsgi_version = ('u', 0) + return LocalWSGISupervisor(**options) + + +class CPWebCase(webtest.WebCase): + + script_name = '' + scheme = 'http' + + available_servers = {'wsgi': LocalWSGISupervisor, + 'wsgi_u': get_wsgi_u_supervisor, + 'native': NativeServerSupervisor, + 'cpmodpy': get_cpmodpy_supervisor, + 'modpygw': get_modpygw_supervisor, + 'modwsgi': get_modwsgi_supervisor, + 'modfcgid': get_modfcgid_supervisor, + 'modfastcgi': get_modfastcgi_supervisor, + } + default_server = 'wsgi' + + @classmethod + def _setup_server(cls, supervisor, conf): + v = sys.version.split()[0] + log.info('Python version used to run this test script: %s' % v) + log.info('CherryPy version: %s' % cherrypy.__version__) + if supervisor.scheme == 'https': + ssl = ' (ssl)' + else: + ssl = '' + log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl)) + log.info('PID: %s' % os.getpid()) + + cherrypy.server.using_apache = supervisor.using_apache + cherrypy.server.using_wsgi = supervisor.using_wsgi + + if sys.platform[:4] == 'java': + cherrypy.config.update({'server.nodelay': False}) + + if isinstance(conf, text_or_bytes): + parser = cherrypy.lib.reprconf.Parser() + conf = parser.dict_from_file(conf).get('global', {}) + else: + conf = conf or {} + baseconf = conf.copy() + baseconf.update({'server.socket_host': supervisor.host, + 'server.socket_port': supervisor.port, + 'server.protocol_version': supervisor.protocol, + 'environment': 'test_suite', + }) + if supervisor.scheme == 'https': + # baseconf['server.ssl_module'] = 'builtin' + baseconf['server.ssl_certificate'] = serverpem + baseconf['server.ssl_private_key'] = serverpem + + # helper must be imported lazily so the coverage tool + # can run against module-level statements within cherrypy. + # Also, we have to do "from cherrypy.test import helper", + # exactly like each test module does, because a relative import + # would stick a second instance of webtest in sys.modules, + # and we wouldn't be able to globally override the port anymore. + if supervisor.scheme == 'https': + webtest.WebCase.HTTP_CONN = HTTPSConnection + return baseconf + + @classmethod + def setup_class(cls): + '' + # Creates a server + conf = { + 'scheme': 'http', + 'protocol': 'HTTP/1.1', + 'port': 54583, + 'host': '127.0.0.1', + 'validate': False, + 'server': 'wsgi', + } + supervisor_factory = cls.available_servers.get( + conf.get('server', 'wsgi')) + if supervisor_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + supervisor = supervisor_factory(**conf) + + # Copied from "run_test_suite" + cherrypy.config.reset() + baseconf = cls._setup_server(supervisor, conf) + cherrypy.config.update(baseconf) + setup_client() + + if hasattr(cls, 'setup_server'): + # Clear the cherrypy tree and clear the wsgi server so that + # it can be updated with the new root + cherrypy.tree = cherrypy._cptree.Tree() + cherrypy.server.httpserver = None + cls.setup_server() + # Add a resource for verifying there are no refleaks + # to *every* test class. + cherrypy.tree.mount(gctools.GCRoot(), '/gc') + cls.do_gc_test = True + supervisor.start(cls.__module__) + + cls.supervisor = supervisor + + @classmethod + def teardown_class(cls): + '' + if hasattr(cls, 'setup_server'): + cls.supervisor.stop() + + do_gc_test = False + + def test_gc(self): + if not self.do_gc_test: + return + + self.getPage('/gc/stats') + try: + self.assertBody('Statistics:') + except Exception: + 'Failures occur intermittently. See #1420' + + def prefix(self): + return self.script_name.rstrip('/') + + def base(self): + if ((self.scheme == 'http' and self.PORT == 80) or + (self.scheme == 'https' and self.PORT == 443)): + port = '' + else: + port = ':%s' % self.PORT + + return '%s://%s%s%s' % (self.scheme, self.HOST, port, + self.script_name.rstrip('/')) + + def exit(self): + sys.exit() + + def getPage(self, url, *args, **kwargs): + """Open the url. + """ + if self.script_name: + url = httputil.urljoin(self.script_name, url) + return webtest.WebCase.getPage(self, url, *args, **kwargs) + + def skip(self, msg='skipped '): + pytest.skip(msg) + + def assertErrorPage(self, status, message=None, pattern=''): + """Compare the response body with a built in error page. + + The function will optionally look for the regexp pattern, + within the exception embedded in the error page.""" + + # This will never contain a traceback + page = cherrypy._cperror.get_error_page(status, message=message) + + # First, test the response body without checking the traceback. + # Stick a match-all group (.*) in to grab the traceback. + def esc(text): + return re.escape(ntob(text)) + epage = re.escape(page) + epage = epage.replace( + esc('
'),
+            esc('
') + b'(.*)' + esc('
')) + m = re.match(epage, self.body, re.DOTALL) + if not m: + self._handlewebError( + 'Error page does not match; expected:\n' + page) + return + + # Now test the pattern against the traceback + if pattern is None: + # Special-case None to mean that there should be *no* traceback. + if m and m.group(1): + self._handlewebError('Error page contains traceback') + else: + if (m is None) or ( + not re.search(ntob(re.escape(pattern), self.encoding), + m.group(1))): + msg = 'Error page does not contain %s in traceback' + self._handlewebError(msg % repr(pattern)) + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +def _test_method_sorter(_, x, y): + """Monkeypatch the test sorter to always run test_gc last in each suite.""" + if x == 'test_gc': + return 1 + if y == 'test_gc': + return -1 + if x > y: + return 1 + if x < y: + return -1 + return 0 + + +unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter + + +def setup_client(): + """Set up the WebCase classes to match the server's socket settings.""" + webtest.WebCase.PORT = cherrypy.server.socket_port + webtest.WebCase.HOST = cherrypy.server.socket_host + if cherrypy.server.ssl_certificate: + CPWebCase.scheme = 'https' + +# --------------------------- Spawning helpers --------------------------- # + + +class CPProcess(object): + + pid_file = os.path.join(thisdir, 'test.pid') + config_file = os.path.join(thisdir, 'test.conf') + config_template = """[global] +server.socket_host: '%(host)s' +server.socket_port: %(port)s +checker.on: False +log.screen: False +log.error_file: r'%(error_log)s' +log.access_file: r'%(access_log)s' +%(ssl)s +%(extra)s +""" + error_log = os.path.join(thisdir, 'test.error.log') + access_log = os.path.join(thisdir, 'test.access.log') + + def __init__(self, wait=False, daemonize=False, ssl=False, + socket_host=None, socket_port=None): + self.wait = wait + self.daemonize = daemonize + self.ssl = ssl + self.host = socket_host or cherrypy.server.socket_host + self.port = socket_port or cherrypy.server.socket_port + + def write_conf(self, extra=''): + if self.ssl: + serverpem = os.path.join(thisdir, 'test.pem') + ssl = """ +server.ssl_certificate: r'%s' +server.ssl_private_key: r'%s' +""" % (serverpem, serverpem) + else: + ssl = '' + + conf = self.config_template % { + 'host': self.host, + 'port': self.port, + 'error_log': self.error_log, + 'access_log': self.access_log, + 'ssl': ssl, + 'extra': extra, + } + with io.open(self.config_file, 'w', encoding='utf-8') as f: + f.write(str(conf)) + + def start(self, imports=None): + """Start cherryd in a subprocess.""" + portend.free(self.host, self.port, timeout=1) + + args = [ + '-m', + 'cherrypy', + '-c', self.config_file, + '-p', self.pid_file, + ] + r""" + Command for running cherryd server with autoreload enabled + + Using + + ``` + ['-c', + "__requires__ = 'CherryPy'; \ + import pkg_resources, re, sys; \ + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ + sys.exit(\ + pkg_resources.load_entry_point(\ + 'CherryPy', 'console_scripts', 'cherryd')())"] + ``` + + doesn't work as it's impossible to reconstruct the `-c`'s contents. + Ref: https://github.com/cherrypy/cherrypy/issues/1545 + """ + + if not isinstance(imports, (list, tuple)): + imports = [imports] + for i in imports: + if i: + args.append('-i') + args.append(i) + + if self.daemonize: + args.append('-d') + + env = os.environ.copy() + # Make sure we import the cherrypy package in which this module is + # defined. + grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) + if env.get('PYTHONPATH', ''): + env['PYTHONPATH'] = os.pathsep.join( + (grandparentdir, env['PYTHONPATH'])) + else: + env['PYTHONPATH'] = grandparentdir + self._proc = subprocess.Popen([sys.executable] + args, env=env) + if self.wait: + self.exit_code = self._proc.wait() + else: + portend.occupied(self.host, self.port, timeout=5) + + # Give the engine a wee bit more time to finish STARTING + if self.daemonize: + time.sleep(2) + else: + time.sleep(1) + + def get_pid(self): + if self.daemonize: + with open(self.pid_file, 'rb') as f: + return int(f.read()) + return self._proc.pid + + def join(self): + """Wait for the process to exit.""" + if self.daemonize: + return self._join_daemon() + self._proc.wait() + + def _join_daemon(self): + with contextlib.suppress(IOError): + os.waitpid(self.get_pid(), 0) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/logtest.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/logtest.py new file mode 100644 index 000000000..112bdc259 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/logtest.py @@ -0,0 +1,236 @@ +"""logtest, a unittest.TestCase helper for testing log output.""" + +import sys +import time +from uuid import UUID + +import pytest + +from cherrypy._cpcompat import text_or_bytes + + +try: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class LogCase(object): + + """unittest.TestCase mixin for testing log messages. + + logfile: a filename for the desired log. Yes, I know modes are evil, + but it makes the test functions so much cleaner to set this once. + + lastmarker: the last marker in the log. This can be used to search for + messages since the last marker. + + markerPrefix: a string with which to prefix log markers. This should be + unique enough from normal log output to use for marker identification. + """ + + interactive = False + logfile = None + lastmarker = None + markerPrefix = b'test suite marker: ' + + def _handleLogError(self, msg, data, marker, pattern): + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise pytest.fail(msg) + + p = (' Show: ' + '[L]og [M]arker [P]attern; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ') + sys.stdout.write(p + ' ') + # ARGH + sys.stdout.flush() + while True: + i = getchar().upper() + if i not in 'MPLIRX': + continue + print(i.upper()) # Also prints new line + if i == 'L': + for x, line in enumerate(data): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r ') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r ') + if m == 'q': + break + print(line.rstrip()) + elif i == 'M': + print(repr(marker or self.lastmarker)) + elif i == 'P': + print(repr(pattern)) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise pytest.fail(msg) + elif i == 'X': + self.exit() + sys.stdout.write(p + ' ') + + def exit(self): + sys.exit() + + def emptyLog(self): + """Overwrite self.logfile with 0 bytes.""" + with open(self.logfile, 'wb') as f: + f.write('') + + def markLog(self, key=None): + """Insert a marker line into the log and set self.lastmarker.""" + if key is None: + key = str(time.time()) + self.lastmarker = key + + with open(self.logfile, 'ab+') as f: + f.write( + b'%s%s\n' + % (self.markerPrefix, key.encode('utf-8')) + ) + + def _read_marked_region(self, marker=None): + """Return lines from self.logfile in the marked region. + + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be returned. + """ +# Give the logger time to finish writing? +# time.sleep(0.5) + + logfile = self.logfile + marker = marker or self.lastmarker + if marker is None: + with open(logfile, 'rb') as f: + return f.readlines() + + if isinstance(marker, str): + marker = marker.encode('utf-8') + data = [] + in_region = False + with open(logfile, 'rb') as f: + for line in f: + if in_region: + if (line.startswith(self.markerPrefix) + and marker not in line): + break + else: + data.append(line) + elif marker in line: + in_region = True + return data + + def assertInLog(self, line, marker=None): + """Fail if the given (partial) line is not in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + return + msg = '%r not found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertNotInLog(self, line, marker=None): + """Fail if the given (partial) line is in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + msg = '%r found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertValidUUIDv4(self, marker=None): + """Fail if the given UUIDv4 is not valid. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + data = [ + chunk.decode('utf-8').rstrip('\n').rstrip('\r') + for chunk in data + ] + for log_chunk in data: + try: + uuid_log = data[-1] + uuid_obj = UUID(uuid_log, version=4) + except (TypeError, ValueError): + pass # it might be in other chunk + else: + if str(uuid_obj) == uuid_log: + return + msg = '%r is not a valid UUIDv4' % uuid_log + self._handleLogError(msg, data, marker, log_chunk) + + msg = 'UUIDv4 not found in log' + self._handleLogError(msg, data, marker, log_chunk) + + def assertLog(self, sliceargs, lines, marker=None): + """Fail if log.readlines()[sliceargs] is not contained in 'lines'. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + if isinstance(sliceargs, int): + # Single arg. Use __getitem__ and allow lines to be str or list. + if isinstance(lines, (tuple, list)): + lines = lines[0] + if isinstance(lines, str): + lines = lines.encode('utf-8') + if lines not in data[sliceargs]: + msg = '%r not found on log line %r' % (lines, sliceargs) + self._handleLogError( + msg, + [data[sliceargs], '--EXTRA CONTEXT--'] + data[ + sliceargs + 1:sliceargs + 6], + marker, + lines) + else: + # Multiple args. Use __getslice__ and require lines to be list. + if isinstance(lines, tuple): + lines = list(lines) + elif isinstance(lines, text_or_bytes): + raise TypeError("The 'lines' arg must be a list when " + "'sliceargs' is a tuple.") + + start, stop = sliceargs + for line, logline in zip(lines, data[start:stop]): + if isinstance(line, str): + line = line.encode('utf-8') + if line not in logline: + msg = '%r not found in log' % line + self._handleLogError(msg, data[start:stop], marker, line) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfastcgi.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfastcgi.py new file mode 100644 index 000000000..16927d3cc --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfastcgi.py @@ -0,0 +1,133 @@ +"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. + +To autostart fastcgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.dev/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'apache2ctl' +CONF_PATH = 'fastcgi.conf' + +conf_fastcgi = """ +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog %(root)s/mod_fastcgi.error.log + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +def erase_script_name(environ, start_response): + environ['SCRIPT_NAME'] = '' + return cherrypy.tree(environ, start_response) + + +class ModFCGISupervisor(helper.LocalWSGISupervisor): + + httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' + using_apache = True + using_wsgi = True + template = conf_fastcgi + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=erase_script_name, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + cherrypy.server.socket_port = 4000 + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + cherrypy.engine.start() + self.sync_apps() + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + with open(fcgiconf, 'wb') as f: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = output.replace('\r\n', '\n') + f.write(output) + + result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalWSGISupervisor.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app( + erase_script_name) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfcgid.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfcgid.py new file mode 100644 index 000000000..6dd48a943 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modfcgid.py @@ -0,0 +1,121 @@ +"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. + +To autostart fcgid, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.dev/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'fcgi.conf' + +conf_fcgid = """ +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +class ModFCGISupervisor(helper.LocalSupervisor): + + using_apache = True + using_wsgi = True + template = conf_fcgid + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + helper.LocalServer.start(self, modulename) + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + with open(fcgiconf, 'wb') as f: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = ntob(output.replace('\r\n', '\n')) + f.write(output) + + result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalServer.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modpy.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modpy.py new file mode 100644 index 000000000..f4f685232 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modpy.py @@ -0,0 +1,161 @@ +"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. + +To autostart modpython, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + +If you wish to test the WSGI interface instead of our _cpmodpy interface, +you also need the 'modpython_gateway' module at: +http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.dev/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'test_mp.conf' + +conf_modpython_gateway = """ +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::wsgisetup +PythonOption testmod %(modulename)s +PythonHandler modpython_gateway::handler +PythonOption wsgi.application cherrypy::tree +PythonOption socket_host %(host)s +PythonDebug On +""" + +conf_cpmodpy = """ +# Apache2 server conf file for testing CherryPy with _cpmodpy. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::cpmodpysetup +PythonHandler cherrypy._cpmodpy::handler +PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server +PythonOption socket_host %(host)s +PythonDebug On +""" + + +class ModPythonSupervisor(helper.Supervisor): + + using_apache = True + using_wsgi = False + template = None + + def __str__(self): + return 'ModPython Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + with open(mpconf, 'wb') as f: + f.write(self.template % + {'port': self.port, 'modulename': modulename, + 'host': self.host}) + + result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def wsgisetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.log'), + 'environment': 'test_suite', + 'server.socket_host': options['socket_host'], + }) + + modname = options['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.server.unsubscribe() + cherrypy.engine.start() + from mod_python import apache + return apache.OK + + +def cpmodpysetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.log'), + 'environment': 'test_suite', + 'server.socket_host': options['socket_host'], + }) + from mod_python import apache + return apache.OK diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modwsgi.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modwsgi.py new file mode 100644 index 000000000..c32162849 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/modwsgi.py @@ -0,0 +1,151 @@ +"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. + +To autostart modwsgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.dev/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_wsgi will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. When responding with 204 No Content, mod_wsgi adds a Content-Length + header for you. +9. When an error is raised, mod_wsgi has no facility for printing a + traceback as the response content (it's sent to the Apache log instead). +10. Startup and shutdown of Apache when running mod_wsgi seems slow. +""" + +import os +import re +import sys +import time + +import portend + +from cheroot.test import webtest + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.abspath(os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +if sys.platform == 'win32': + APACHE_PATH = 'httpd' +else: + APACHE_PATH = 'apache' + +CONF_PATH = 'test_mw.conf' + +conf_modwsgi = r""" +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s + +AllowEncodedSlashes On +LoadModule rewrite_module modules/mod_rewrite.so +RewriteEngine on +RewriteMap escaping int:escape + +LoadModule log_config_module modules/mod_log_config.so +LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined +CustomLog "%(curdir)s/apache.access.log" combined +ErrorLog "%(curdir)s/apache.error.log" +LogLevel debug + +LoadModule wsgi_module modules/mod_wsgi.so +LoadModule env_module modules/mod_env.so + +WSGIScriptAlias / "%(curdir)s/modwsgi.py" +SetEnv testmod %(testmod)s +""" # noqa E501 + + +class ModWSGISupervisor(helper.Supervisor): + + """Server Controller for ModWSGI and CherryPy.""" + + using_apache = True + using_wsgi = True + template = conf_modwsgi + + def __str__(self): + return 'ModWSGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + with open(mpconf, 'wb') as f: + output = (self.template % + {'port': self.port, 'testmod': modulename, + 'curdir': curdir}) + f.write(output) + + result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + if result: + print(result) + + # Make a request so mod_wsgi starts up our app. + # If we don't, concurrent initial requests will 404. + portend.occupied('127.0.0.1', self.port, timeout=5) + webtest.openURL('/ihopetheresnodefault', port=self.port) + time.sleep(1) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def application(environ, start_response): + global loaded + if not loaded: + loaded = True + modname = 'cherrypy.test.' + environ['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.error.log'), + 'log.access_file': os.path.join(curdir, 'test.access.log'), + 'environment': 'test_suite', + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }) + return cherrypy.tree(environ, start_response) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/sessiondemo.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/sessiondemo.py new file mode 100644 index 000000000..3849a2593 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/sessiondemo.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +"""A session demonstration app.""" + +import calendar +from datetime import datetime +import sys + +import cherrypy +from cherrypy.lib import sessions + + +page = """ + + + + + + + +

Session Demo

+

Reload this page. The session ID should not change from one reload to the next

+

Index | Expire | Regenerate

+ + + + + + + + + +
Session ID:%(sessionid)s

%(changemsg)s

Request Cookie%(reqcookie)s
Response Cookie%(respcookie)s

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
+ +""" # noqa E501 + + +class Root(object): + + def page(self): + changemsg = [] + if cherrypy.session.id != cherrypy.session.originalid: + if cherrypy.session.originalid is None: + changemsg.append( + 'Created new session because no session id was given.') + if cherrypy.session.missing: + changemsg.append( + 'Created new session due to missing ' + '(expired or malicious) session.') + if cherrypy.session.regenerated: + changemsg.append('Application generated a new session.') + + try: + expires = cherrypy.response.cookie['session_id']['expires'] + except KeyError: + expires = '' + + return page % { + 'sessionid': cherrypy.session.id, + 'changemsg': '
'.join(changemsg), + 'respcookie': cherrypy.response.cookie.output(), + 'reqcookie': cherrypy.request.cookie.output(), + 'sessiondata': list(cherrypy.session.items()), + 'servertime': ( + datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' + ), + 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), + 'cpversion': cherrypy.__version__, + 'pyversion': sys.version, + 'expires': expires, + } + + @cherrypy.expose + def index(self): + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'green' + return self.page() + + @cherrypy.expose + def expire(self): + sessions.expire() + return self.page() + + @cherrypy.expose + def regen(self): + cherrypy.session.regenerate() + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'yellow' + return self.page() + + +if __name__ == '__main__': + cherrypy.config.update({ + # 'environment': 'production', + 'log.screen': True, + 'tools.sessions.on': True, + }) + cherrypy.quickstart(Root()) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/404.html b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/404.html new file mode 100644 index 000000000..01b17b096 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/404.html @@ -0,0 +1,5 @@ + + +

I couldn't find that thing you were looking for!

+ + diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/dirback.jpg b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/dirback.jpg new file mode 100644 index 000000000..80403dc22 Binary files /dev/null and b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/dirback.jpg differ diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/index.html b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/index.html new file mode 100644 index 000000000..b9f5f097d --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/static/index.html @@ -0,0 +1 @@ +Hello, world diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/style.css b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/style.css new file mode 100644 index 000000000..b266e93de --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/style.css @@ -0,0 +1 @@ +Dummy stylesheet diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test.pem b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test.pem new file mode 100644 index 000000000..47a47042b --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_basic.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_basic.py new file mode 100644 index 000000000..f178f8f97 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_basic.py @@ -0,0 +1,135 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +from hashlib import md5 + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import auth_basic +from cherrypy.test import helper + + +class BasicAuthTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'This is public.' + + class BasicProtected: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + class BasicProtected2: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + class BasicProtected2_u: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + userpassdict = {'xuser': 'xpassword'} + userhashdict = {'xuser': md5(b'xpassword').hexdigest()} + userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()} + + def checkpasshash(realm, user, password): + p = userhashdict.get(user) + return p and p == md5(ntob(password)).hexdigest() or False + + def checkpasshash_u(realm, user, password): + p = userhashdict_u.get(user) + return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False + + basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict) + conf = { + '/basic': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': basic_checkpassword_dict + }, + '/basic2': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash, + 'tools.auth_basic.accept_charset': 'ISO-8859-1', + }, + '/basic2_u': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash_u, + 'tools.auth_basic.accept_charset': 'UTF-8', + }, + } + + root = Root() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + root.basic2_u = BasicProtected2_u() + cherrypy.tree.mount(root, config=conf) + + def testPublic(self): + self.getPage('/') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage('/basic/') + self.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2(self): + self.getPage('/basic2/') + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic2/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic2/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2_u(self): + self.getPage('/basic2_u/') + self.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) + self.assertStatus(401) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) + self.assertStatus('200 OK') + self.assertBody("Hello xюзер, you've been authorized.") diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_digest.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_digest.py new file mode 100644 index 000000000..4b7b5298f --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_auth_digest.py @@ -0,0 +1,131 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + + +import cherrypy +from cherrypy.lib import auth_digest +from cherrypy._cpcompat import ntob + +from cherrypy.test import helper + + +def _fetch_users(): + return {'test': 'test', '☃йюзер': 'їпароль'} + + +get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users()) + + +class DigestAuthTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'This is public.' + + class DigestProtected: + + @cherrypy.expose + def index(self, *args, **kwargs): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + conf = {'/digest': {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'localhost', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.debug': True, + 'tools.auth_digest.accept_charset': 'UTF-8'}} + + root = Root() + root.digest = DigestProtected() + cherrypy.tree.mount(root, config=conf) + + def testPublic(self): + self.getPage('/') + assert self.status == '200 OK' + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + assert self.body == b'This is public.' + + def _test_parametric_digest(self, username, realm): + test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path' + + self.getPage(test_uri) + assert self.status_code == 401 + + msg = 'Digest authentification scheme was not found' + www_auth_digest = tuple(filter( + lambda kv: kv[0].lower() == 'www-authenticate' + and kv[1].startswith('Digest '), + self.headers, + )) + assert len(www_auth_digest) == 1, msg + + items = www_auth_digest[0][-1][7:].split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + assert tokens['realm'] == '"localhost"' + assert tokens['algorithm'] == '"MD5"' + assert tokens['qop'] == '"auth"' + assert tokens['charset'] == '"UTF-8"' + + nonce = tokens['nonce'].strip('"') + + # Test user agent response with a wrong value for 'realm' + base_auth = ('Digest username="%s", ' + 'realm="%s", ' + 'nonce="%s", ' + 'uri="%s", ' + 'algorithm=MD5, ' + 'response="%s", ' + 'qop=auth, ' + 'nc=%s, ' + 'cnonce="1522e61005789929"') + + encoded_user = username + encoded_user = encoded_user.encode('utf-8') + encoded_user = encoded_user.decode('latin1') + auth_header = base_auth % ( + encoded_user, realm, nonce, test_uri, + '11111111111111111111111111111111', '00000001', + ) + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1(auth.realm, auth.username) + response = auth.request_digest(ha1) + auth_header = base_auth % ( + encoded_user, realm, nonce, test_uri, + response, '00000001', + ) + self.getPage(test_uri, [('Authorization', auth_header)]) + + def test_wrong_realm(self): + # send response with correct response digest, but wrong realm + self._test_parametric_digest(username='test', realm='wrong realm') + assert self.status_code == 401 + + def test_ascii_user(self): + self._test_parametric_digest(username='test', realm='localhost') + assert self.status == '200 OK' + assert self.body == b"Hello test, you've been authorized." + + def test_unicode_user(self): + self._test_parametric_digest(username='☃йюзер', realm='localhost') + assert self.status == '200 OK' + assert self.body == ntob( + "Hello ☃йюзер, you've been authorized.", 'utf-8', + ) + + def test_wrong_scheme(self): + basic_auth = { + 'Authorization': 'Basic foo:bar', + } + self.getPage('/digest/', headers=list(basic_auth.items())) + assert self.status_code == 401 diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_bus.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_bus.py new file mode 100644 index 000000000..594023a23 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_bus.py @@ -0,0 +1,327 @@ +"""Publish-subscribe bus tests.""" +# pylint: disable=redefined-outer-name + +import os +import sys +import threading +import time +import unittest.mock + +import pytest + +from cherrypy.process import wspbus + + +CI_ON_MACOS = bool(os.getenv('CI')) and sys.platform == 'darwin' +msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name + + +@pytest.fixture +def bus(): + """Return a wspbus instance.""" + return wspbus.Bus() + + +@pytest.fixture +def log_tracker(bus): + """Return an instance of bus log tracker.""" + class LogTracker: # pylint: disable=too-few-public-methods + """Bus log tracker.""" + + log_entries = [] + + def __init__(self, bus): + def logit(msg, level): # pylint: disable=unused-argument + self.log_entries.append(msg) + bus.subscribe('log', logit) + + return LogTracker(bus) + + +@pytest.fixture +def listener(): + """Return an instance of bus response tracker.""" + class Listner: # pylint: disable=too-few-public-methods + """Bus handler return value tracker.""" + + responses = [] + + def get_listener(self, channel, index): + """Return an argument tracking listener.""" + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + return Listner() + + +def test_builtin_channels(bus, listener): + """Test that built-in channels trigger corresponding listeners.""" + expected = [] + + for channel in bus.listeners: + for index, priority in enumerate([100, 50, 0, 51]): + bus.subscribe( + channel, + listener.get_listener(channel, index), + priority, + ) + + for channel in bus.listeners: + bus.publish(channel) + expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) + bus.publish(channel, arg=79347) + expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) + + assert listener.responses == expected + + +def test_custom_channels(bus, listener): + """Test that custom pub-sub channels work as built-in ones.""" + expected = [] + + custom_listeners = ('hugh', 'louis', 'dewey') + for channel in custom_listeners: + for index, priority in enumerate([None, 10, 60, 40]): + bus.subscribe( + channel, + listener.get_listener(channel, index), + priority, + ) + + for channel in custom_listeners: + bus.publish(channel, 'ah so') + expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)) + bus.publish(channel) + expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2)) + + assert listener.responses == expected + + +def test_listener_errors(bus, listener): + """Test that unhandled exceptions raise channel failures.""" + expected = [] + channels = [c for c in bus.listeners if c != 'log'] + + for channel in channels: + bus.subscribe(channel, listener.get_listener(channel, 1)) + # This will break since the lambda takes no args. + bus.subscribe(channel, lambda: None, priority=20) + + for channel in channels: + with pytest.raises(wspbus.ChannelFailures): + bus.publish(channel, 123) + expected.append(msg % (1, channel, 123)) + + assert listener.responses == expected + + +def test_start(bus, listener, log_tracker): + """Test that bus start sequence calls all listeners.""" + num = 3 + for index in range(num): + bus.subscribe('start', listener.get_listener('start', index)) + + bus.start() + try: + # The start method MUST call all 'start' listeners. + assert ( + set(listener.responses) == + set(msg % (i, 'start', None) for i in range(num))) + # The start method MUST move the state to STARTED + # (or EXITING, if errors occur) + assert bus.state == bus.states.STARTED + # The start method MUST log its states. + assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED'] + finally: + # Exit so the atexit handler doesn't complain. + bus.exit() + + +def test_stop(bus, listener, log_tracker): + """Test that bus stop sequence calls all listeners.""" + num = 3 + + for index in range(num): + bus.subscribe('stop', listener.get_listener('stop', index)) + + bus.stop() + + # The stop method MUST call all 'stop' listeners. + assert (set(listener.responses) == + set(msg % (i, 'stop', None) for i in range(num))) + + # The stop method MUST move the state to STOPPED + assert bus.state == bus.states.STOPPED + + # The stop method MUST log its states. + assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED'] + + +def test_graceful(bus, listener, log_tracker): + """Test that bus graceful state triggers all listeners.""" + num = 3 + + for index in range(num): + bus.subscribe('graceful', listener.get_listener('graceful', index)) + + bus.graceful() + + # The graceful method MUST call all 'graceful' listeners. + assert ( + set(listener.responses) == + set(msg % (i, 'graceful', None) for i in range(num))) + + # The graceful method MUST log its states. + assert log_tracker.log_entries == ['Bus graceful'] + + +def test_exit(bus, listener, log_tracker): + """Test that bus exit sequence is correct.""" + num = 3 + + for index in range(num): + bus.subscribe('stop', listener.get_listener('stop', index)) + bus.subscribe('exit', listener.get_listener('exit', index)) + + bus.exit() + + # The exit method MUST call all 'stop' listeners, + # and then all 'exit' listeners. + assert (set(listener.responses) == + set([msg % (i, 'stop', None) for i in range(num)] + + [msg % (i, 'exit', None) for i in range(num)])) + + # The exit method MUST move the state to EXITING + assert bus.state == bus.states.EXITING + + # The exit method MUST log its states. + assert (log_tracker.log_entries == + ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + + +def test_wait(bus): + """Test that bus wait awaits for states.""" + def f(method): # pylint: disable=invalid-name + time.sleep(0.2) + getattr(bus, method)() + + flow = [ + ('start', [bus.states.STARTED]), + ('stop', [bus.states.STOPPED]), + ('start', [bus.states.STARTING, bus.states.STARTED]), + ('exit', [bus.states.EXITING]), + ] + + for method, states in flow: + threading.Thread(target=f, args=(method,)).start() + bus.wait(states) + + # The wait method MUST wait for the given state(s). + assert bus.state in states, 'State %r not in %r' % (bus.state, states) + + +@pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails') +def test_wait_publishes_periodically(bus): + """Test that wait publishes each tick.""" + callback = unittest.mock.MagicMock() + bus.subscribe('main', callback) + + def set_start(): + time.sleep(0.05) + bus.start() + threading.Thread(target=set_start).start() + bus.wait(bus.states.STARTED, interval=0.01, channel='main') + assert callback.call_count > 3 + + +def test_block(bus, log_tracker): + """Test that bus block waits for exiting.""" + def f(): # pylint: disable=invalid-name + time.sleep(0.2) + bus.exit() + + def g(): # pylint: disable=invalid-name + time.sleep(0.4) + + threading.Thread(target=f).start() + threading.Thread(target=g).start() + threads = [t for t in threading.enumerate() if not t.daemon] + assert len(threads) == 3 + + bus.block() + + # The block method MUST wait for the EXITING state. + assert bus.state == bus.states.EXITING + + # The block method MUST wait for ALL non-main, non-daemon threads to + # finish. + threads = [t for t in threading.enumerate() if not t.daemon] + assert len(threads) == 1 + + # The last message will mention an indeterminable thread name; ignore + # it + expected_bus_messages = [ + 'Bus STOPPING', + 'Bus STOPPED', + 'Bus EXITING', + 'Bus EXITED', + 'Waiting for child threads to terminate...', + ] + bus_msg_num = len(expected_bus_messages) + + # If the last message mentions an indeterminable thread name then ignore it + assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages + assert len(log_tracker.log_entries[bus_msg_num:]) <= 1, ( + 'No more than one extra log line with the thread name expected' + ) + + +def test_start_with_callback(bus): + """Test that callback fires on bus start.""" + try: + events = [] + + def f(*args, **kwargs): # pylint: disable=invalid-name + events.append(('f', args, kwargs)) + + def g(): # pylint: disable=invalid-name + events.append('g') + bus.subscribe('start', g) + bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) + + # Give wait() time to run f() + time.sleep(0.2) + + # The callback method MUST wait for the STARTED state. + assert bus.state == bus.states.STARTED + + # The callback method MUST run after all start methods. + assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})] + finally: + bus.exit() + + +def test_log(bus, log_tracker): + """Test that bus messages and errors are logged.""" + assert log_tracker.log_entries == [] + + # Try a normal message. + expected = [] + for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: + bus.log(msg_) + expected.append(msg_) + assert log_tracker.log_entries == expected + + # Try an error message + try: + foo + except NameError: + bus.log('You are lost and gone forever', traceback=True) + lastmsg = log_tracker.log_entries[-1] + assert 'Traceback' in lastmsg and 'NameError' in lastmsg, ( + 'Last log message %r did not contain ' + 'the expected traceback.' % lastmsg + ) + else: + pytest.fail('NameError was not raised as expected.') diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_caching.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_caching.py new file mode 100644 index 000000000..c0b897976 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_caching.py @@ -0,0 +1,390 @@ +import datetime +from itertools import count +import os +import threading +import time +import urllib.parse + +import pytest + +import cherrypy +from cherrypy.lib import httputil + +from cherrypy.test import helper + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +gif_bytes = ( + b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;' +) + + +class CacheTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + @cherrypy.config(**{'tools.caching.on': True}) + class Root: + + def __init__(self): + self.counter = 0 + self.control_counter = 0 + self.longlock = threading.Lock() + + @cherrypy.expose + def index(self): + self.counter += 1 + msg = 'visit #%s' % self.counter + return msg + + @cherrypy.expose + def control(self): + self.control_counter += 1 + return 'visit #%s' % self.control_counter + + @cherrypy.expose + def a_gif(self): + cherrypy.response.headers[ + 'Last-Modified'] = httputil.HTTPDate() + return gif_bytes + + @cherrypy.expose + def long_process(self, seconds='1'): + try: + self.longlock.acquire() + time.sleep(float(seconds)) + finally: + self.longlock.release() + return 'success!' + + @cherrypy.expose + def clear_cache(self, path): + cherrypy._cache.store[cherrypy.request.base + path].clear() + + @cherrypy.config(**{ + 'tools.caching.on': True, + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [ + ('Vary', 'Our-Varying-Header') + ], + }) + class VaryHeaderCachingServer(object): + + def __init__(self): + self.counter = count(1) + + @cherrypy.expose + def index(self): + return 'visit #%s' % next(self.counter) + + @cherrypy.config(**{ + 'tools.expires.on': True, + 'tools.expires.secs': 60, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }) + class UnCached(object): + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': 0}) + def force(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + self._cp_config['tools.expires.force'] = True + self._cp_config['tools.expires.secs'] = 0 + return 'being forceful' + + @cherrypy.expose + def dynamic(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + cherrypy.response.headers['Cache-Control'] = 'private' + return 'D-d-d-dynamic!' + + @cherrypy.expose + def cacheable(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + return "Hi, I'm cacheable." + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': 86400}) + def specific(self): + cherrypy.response.headers[ + 'Etag'] = 'need_this_to_make_me_cacheable' + return 'I am being specific' + + class Foo(object): + pass + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': Foo()}) + def wrongtype(self): + cherrypy.response.headers[ + 'Etag'] = 'need_this_to_make_me_cacheable' + return 'Woops' + + @cherrypy.config(**{ + 'tools.gzip.mime_types': ['text/*', 'image/*'], + 'tools.caching.on': True, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir + }) + class GzipStaticCache(object): + pass + + cherrypy.tree.mount(Root()) + cherrypy.tree.mount(UnCached(), '/expires') + cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers') + cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache') + cherrypy.config.update({'tools.gzip.on': True}) + + def testCaching(self): + elapsed = 0.0 + for trial in range(10): + self.getPage('/') + # The response should be the same every time, + # except for the Age response header. + self.assertBody('visit #1') + if trial != 0: + age = int(self.assertHeader('Age')) + assert age >= elapsed + elapsed = age + + # POST, PUT, DELETE should not be cached. + self.getPage('/', method='POST') + self.assertBody('visit #2') + # Because gzip is turned on, the Vary header should always Vary for + # content-encoding + self.assertHeader('Vary', 'Accept-Encoding') + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage('/', method='GET') + self.assertBody('visit #3') + # ...but this request should get the cached copy. + self.getPage('/', method='GET') + self.assertBody('visit #3') + self.getPage('/', method='DELETE') + self.assertBody('visit #4') + + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertHeader('Vary') + self.assertEqual( + cherrypy.lib.encoding.decompress(self.body), b'visit #5') + + # Now check that a second request gets the gzip header and gzipped body + # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped + # response body was being gzipped a second time. + self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertEqual( + cherrypy.lib.encoding.decompress(self.body), b'visit #5') + + # Now check that a third request that doesn't accept gzip + # skips the cache (because the 'Vary' header denies it). + self.getPage('/', method='GET') + self.assertNoHeader('Content-Encoding') + self.assertBody('visit #6') + + def testVaryHeader(self): + self.getPage('/varying_headers/') + self.assertStatus('200 OK') + self.assertHeaderItemValue('Vary', 'Our-Varying-Header') + self.assertBody('visit #1') + + # Now check that different 'Vary'-fields don't evict each other. + # This test creates 2 requests with different 'Our-Varying-Header' + # and then tests if the first one still exists. + self.getPage('/varying_headers/', + headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus('200 OK') + self.assertBody('visit #2') + + self.getPage('/varying_headers/', + headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus('200 OK') + self.assertBody('visit #2') + + self.getPage('/varying_headers/') + self.assertStatus('200 OK') + self.assertBody('visit #1') + + def testExpiresTool(self): + # test setting an expires header + self.getPage('/expires/specific') + self.assertStatus('200 OK') + self.assertHeader('Expires') + + # test exceptions for bad time values + self.getPage('/expires/wrongtype') + self.assertStatus(500) + self.assertInBody('TypeError') + + # static content should not have "cache prevention" headers + self.getPage('/expires/index.html') + self.assertStatus('200 OK') + self.assertNoHeader('Pragma') + self.assertNoHeader('Cache-Control') + self.assertHeader('Expires') + + # dynamic content that sets indicators should not have + # "cache prevention" headers + self.getPage('/expires/cacheable') + self.assertStatus('200 OK') + self.assertNoHeader('Pragma') + self.assertNoHeader('Cache-Control') + self.assertHeader('Expires') + + self.getPage('/expires/dynamic') + self.assertBody('D-d-d-dynamic!') + # the Cache-Control header should be untouched + self.assertHeader('Cache-Control', 'private') + self.assertHeader('Expires') + + # configure the tool to ignore indicators and replace existing headers + self.getPage('/expires/force') + self.assertStatus('200 OK') + # This also gives us a chance to test 0 expiry with no other headers + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + # static content should now have "cache prevention" headers + self.getPage('/expires/index.html') + self.assertStatus('200 OK') + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + # the cacheable handler should now have "cache prevention" headers + self.getPage('/expires/cacheable') + self.assertStatus('200 OK') + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + self.getPage('/expires/dynamic') + self.assertBody('D-d-d-dynamic!') + # dynamic sets Cache-Control to private but it should be + # overwritten here ... + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + def _assert_resp_len_and_enc_for_gzip(self, uri): + """ + Test that after querying gzipped content it's remains valid in + cache and available non-gzipped as well. + """ + ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')] + content_len = None + + for _ in range(3): + self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS) + + if content_len is not None: + # all requests should get the same length + self.assertHeader('Content-Length', content_len) + self.assertHeader('Content-Encoding', 'gzip') + + content_len = dict(self.headers)['Content-Length'] + + # check that we can still get non-gzipped version + self.getPage(uri, method='GET') + self.assertNoHeader('Content-Encoding') + # non-gzipped version should have a different content length + self.assertNoHeaderItemValue('Content-Length', content_len) + + def testGzipStaticCache(self): + """Test that cache and gzip tools play well together when both enabled. + + Ref GitHub issue #1190. + """ + GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}' + resource_files = ('index.html', 'dirback.jpg') + + for f in resource_files: + uri = GZIP_STATIC_CACHE_TMPL.format(f) + self._assert_resp_len_and_enc_for_gzip(uri) + + def testLastModified(self): + self.getPage('/a.gif') + self.assertStatus(200) + self.assertBody(gif_bytes) + lm1 = self.assertHeader('Last-Modified') + + # this request should get the cached copy. + self.getPage('/a.gif') + self.assertStatus(200) + self.assertBody(gif_bytes) + self.assertHeader('Age') + lm2 = self.assertHeader('Last-Modified') + self.assertEqual(lm1, lm2) + + # this request should match the cached copy, but raise 304. + self.getPage('/a.gif', [('If-Modified-Since', lm1)]) + self.assertStatus(304) + self.assertNoHeader('Last-Modified') + if not getattr(cherrypy.server, 'using_apache', False): + self.assertHeader('Age') + + @pytest.mark.xfail(reason='#1536') + def test_antistampede(self): + SECONDS = 4 + slow_url = '/long_process?seconds={SECONDS}'.format(**locals()) + # We MUST make an initial synchronous request in order to create the + # AntiStampedeCache object, and populate its selecting_headers, + # before the actual stampede. + self.getPage(slow_url) + self.assertBody('success!') + path = urllib.parse.quote(slow_url, safe='') + self.getPage('/clear_cache?path=' + path) + self.assertStatus(200) + + start = datetime.datetime.now() + + def run(): + self.getPage(slow_url) + # The response should be the same every time + self.assertBody('success!') + ts = [threading.Thread(target=run) for i in range(100)] + for t in ts: + t.start() + for t in ts: + t.join() + finish = datetime.datetime.now() + # Allow for overhead, two seconds for slow hosts + allowance = SECONDS + 2 + self.assertEqualDates(start, finish, seconds=allowance) + + def test_cache_control(self): + self.getPage('/control') + self.assertBody('visit #1') + self.getPage('/control') + self.assertBody('visit #1') + + self.getPage('/control', headers=[('Cache-Control', 'no-cache')]) + self.assertBody('visit #2') + self.getPage('/control') + self.assertBody('visit #2') + + self.getPage('/control', headers=[('Pragma', 'no-cache')]) + self.assertBody('visit #3') + self.getPage('/control') + self.assertBody('visit #3') + + time.sleep(1) + self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) + self.assertBody('visit #4') + self.getPage('/control') + self.assertBody('visit #4') diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config.py new file mode 100644 index 000000000..ecd460198 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config.py @@ -0,0 +1,291 @@ +"""Tests for the CherryPy configuration system.""" + +import io +import os +import sys +import unittest + +import cherrypy + +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def StringIOFromNative(x): + return io.StringIO(str(x)) + + +def setup_server(): + + @cherrypy.config(foo='this', bar='that') + class Root: + + def __init__(self): + cherrypy.config.namespaces['db'] = self.db_namespace + + def db_namespace(self, k, v): + if k == 'scheme': + self.db = v + + @cherrypy.expose(alias=('global_', 'xyz')) + def index(self, key): + return cherrypy.request.config.get(key, 'None') + + @cherrypy.expose + def repr(self, key): + return repr(cherrypy.request.config.get(key, None)) + + @cherrypy.expose + def dbscheme(self): + return self.db + + @cherrypy.expose + @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']}) + def plain(self, x): + return x + + favicon_ico = cherrypy.tools.staticfile.handler( + filename=os.path.join(localDir, '../favicon.ico')) + + @cherrypy.config(foo='this2', baz='that2') + class Foo: + + @cherrypy.expose + def index(self, key): + return cherrypy.request.config.get(key, 'None') + nex = index + + @cherrypy.expose + @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) + def silly(self): + return 'Hello world' + + # Test the expose and config decorators + @cherrypy.config(foo='this3', **{'bax': 'this4'}) + @cherrypy.expose + def bar(self, key): + return repr(cherrypy.request.config.get(key, None)) + + class Another: + + @cherrypy.expose + def index(self, key): + return str(cherrypy.request.config.get(key, 'None')) + + def raw_namespace(key, value): + if key == 'input.map': + handler = cherrypy.request.handler + + def wrapper(): + params = cherrypy.request.params + for name, coercer in value.copy().items(): + try: + params[name] = coercer(params[name]) + except KeyError: + pass + return handler() + cherrypy.request.handler = wrapper + elif key == 'output': + handler = cherrypy.request.handler + + def wrapper(): + # 'value' is a type (like int or str). + return value(handler()) + cherrypy.request.handler = wrapper + + @cherrypy.config(**{'raw.output': repr}) + class Raw: + + @cherrypy.expose + @cherrypy.config(**{'raw.input.map': {'num': int}}) + def incr(self, num): + return num + 1 + + ioconf = StringIOFromNative(""" +[/] +neg: -1234 +filename: os.path.join(sys.prefix, "hello.py") +thing1: cherrypy.lib.httputil.response_codes[404] +thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 +complex: 3+2j +mul: 6*3 +ones: "11" +twos: "22" +stradd: %%(ones)s + %%(twos)s + "33" + +[/favicon.ico] +tools.staticfile.filename = %r +""" % os.path.join(localDir, 'static/dirback.jpg')) + + root = Root() + root.foo = Foo() + root.raw = Raw() + app = cherrypy.tree.mount(root, config=ioconf) + app.request_class.namespaces['raw'] = raw_namespace + + cherrypy.tree.mount(Another(), '/another') + cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', + 'db.scheme': r'sqlite///memory', + }) + + +# Client-side code # + + +class ConfigTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testConfig(self): + tests = [ + ('/', 'nex', 'None'), + ('/', 'foo', 'this'), + ('/', 'bar', 'that'), + ('/xyz', 'foo', 'this'), + ('/foo/', 'foo', 'this2'), + ('/foo/', 'bar', 'that'), + ('/foo/', 'bax', 'None'), + ('/foo/bar', 'baz', "'that2'"), + ('/foo/nex', 'baz', 'that2'), + # If 'foo' == 'this', then the mount point '/another' leaks into + # '/'. + ('/another/', 'foo', 'None'), + ] + for path, key, expected in tests: + self.getPage(path + '?key=' + key) + self.assertBody(expected) + + expectedconf = { + # From CP defaults + 'tools.log_headers.on': False, + 'tools.log_tracebacks.on': True, + 'request.show_tracebacks': True, + 'log.screen': False, + 'environment': 'test_suite', + 'engine.autoreload.on': False, + # From global config + 'luxuryyacht': 'throatwobblermangrove', + # From Root._cp_config + 'bar': 'that', + # From Foo._cp_config + 'baz': 'that2', + # From Foo.bar._cp_config + 'foo': 'this3', + 'bax': 'this4', + } + for key, expected in expectedconf.items(): + self.getPage('/foo/bar?key=' + key) + self.assertBody(repr(expected)) + + def testUnrepr(self): + self.getPage('/repr?key=neg') + self.assertBody('-1234') + + self.getPage('/repr?key=filename') + self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) + + self.getPage('/repr?key=thing1') + self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) + + if not getattr(cherrypy.server, 'using_apache', False): + # The object ID's won't match up when using Apache, since the + # server and client are running in different processes. + self.getPage('/repr?key=thing2') + from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) + + self.getPage('/repr?key=complex') + self.assertBody('(3+2j)') + + self.getPage('/repr?key=mul') + self.assertBody('18') + + self.getPage('/repr?key=stradd') + self.assertBody(repr('112233')) + + def testRespNamespaces(self): + self.getPage('/foo/silly') + self.assertHeader('X-silly', 'sillyval') + self.assertBody('Hello world') + + def testCustomNamespaces(self): + self.getPage('/raw/incr?num=12') + self.assertBody('13') + + self.getPage('/dbscheme') + self.assertBody(r'sqlite///memory') + + def testHandlerToolConfigOverride(self): + # Assert that config overrides tool constructor args. Above, we set + # the favicon in the page handler to be '../favicon.ico', + # but then overrode it in config to be './static/dirback.jpg'. + self.getPage('/favicon.ico') + with open(os.path.join(localDir, 'static/dirback.jpg'), 'rb') as tf: + self.assertBody(tf.read()) + + def test_request_body_namespace(self): + self.getPage('/plain', method='POST', headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', '13')], + body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00') + self.assertBody('abc') + + +class VariableSubstitutionTests(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_config(self): + from textwrap import dedent + + # variable substitution with [DEFAULT] + conf = dedent(""" + [DEFAULT] + dir = "/some/dir" + my.dir = %(dir)s + "/sub" + + [my] + my.dir = %(dir)s + "/my/dir" + my.dir2 = %(my.dir)s + '/dir2' + + """) + + fp = StringIOFromNative(conf) + + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir') + self.assertEqual(cherrypy.config['my'] + ['my.dir2'], '/some/dir/my/dir/dir2') + + +class CallablesInConfigTest(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_call_with_literal_dict(self): + from textwrap import dedent + conf = dedent(""" + [my] + value = dict(**{'foo': 'bar'}) + """) + fp = StringIOFromNative(conf) + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'}) + + def test_call_with_kwargs(self): + from textwrap import dedent + conf = dedent(""" + [my] + value = dict(foo="buzz", **cherrypy._test_dict) + """) + test_dict = { + 'foo': 'bar', + 'bar': 'foo', + 'fizz': 'buzz' + } + cherrypy._test_dict = test_dict + fp = StringIOFromNative(conf) + cherrypy.config.update(fp) + test_dict['foo'] = 'buzz' + self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz') + self.assertEqual(cherrypy.config['my']['value'], test_dict) + del cherrypy._test_dict diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config_server.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config_server.py new file mode 100644 index 000000000..7b1835304 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_config_server.py @@ -0,0 +1,126 @@ +"""Tests for the CherryPy configuration system.""" + +import os + +import cherrypy +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +# Client-side code # + + +class ServerConfigTests(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class Root: + + @cherrypy.expose + def index(self): + return cherrypy.request.wsgi_environ['SERVER_PORT'] + + @cherrypy.expose + def upload(self, file): + return 'Size: %s' % len(file.file.read()) + + @cherrypy.expose + @cherrypy.config(**{'request.body.maxbytes': 100}) + def tinyupload(self): + return cherrypy.request.body.read() + + cherrypy.tree.mount(Root()) + + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': 9876, + 'server.max_request_body_size': 200, + 'server.max_request_header_size': 500, + 'server.socket_timeout': 0.5, + + # Test explicit server.instance + 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', + 'server.2.socket_port': 9877, + + # Test non-numeric + # Also test default server.instance = builtin server + 'server.yetanother.socket_port': 9878, + }) + + PORT = 9876 + + def testBasicConfig(self): + self.getPage('/') + self.assertBody(str(self.PORT)) + + def testAdditionalServers(self): + if self.scheme == 'https': + return self.skip('not available under ssl') + self.PORT = 9877 + self.getPage('/') + self.assertBody(str(self.PORT)) + self.PORT = 9878 + self.getPage('/') + self.assertBody(str(self.PORT)) + + def testMaxRequestSizePerHandler(self): + if getattr(cherrypy.server, 'using_apache', False): + return self.skip('skipped due to known Apache differences... ') + + self.getPage('/tinyupload', method='POST', + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '100')], + body='x' * 100) + self.assertStatus(200) + self.assertBody('x' * 100) + + self.getPage('/tinyupload', method='POST', + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '101')], + body='x' * 101) + self.assertStatus(413) + + def testMaxRequestSize(self): + if getattr(cherrypy.server, 'using_apache', False): + return self.skip('skipped due to known Apache differences... ') + + for size in (500, 5000, 50000): + self.getPage('/', headers=[('From', 'x' * 500)]) + self.assertStatus(413) + + # Test for https://github.com/cherrypy/cherrypy/issues/421 + # (Incorrect border condition in readline of SizeCheckWrapper). + # This hangs in rev 891 and earlier. + lines256 = 'x' * 248 + self.getPage('/', + headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), + ('From', lines256)]) + + # Test upload + cd = ( + 'Content-Disposition: form-data; ' + 'name="file"; ' + 'filename="hello.txt"' + ) + body = '\r\n'.join([ + '--x', + cd, + 'Content-Type: text/plain', + '', + '%s', + '--x--']) + partlen = 200 - len(body) + b = body % ('x' * partlen) + h = [('Content-type', 'multipart/form-data; boundary=x'), + ('Content-Length', '%s' % len(b))] + self.getPage('/upload', h, 'POST', b) + self.assertBody('Size: %d' % partlen) + + b = body % ('x' * 200) + h = [('Content-type', 'multipart/form-data; boundary=x'), + ('Content-Length', '%s' % len(b))] + self.getPage('/upload', h, 'POST', b) + self.assertStatus(413) diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_conn.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_conn.py new file mode 100644 index 000000000..e4426c422 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_conn.py @@ -0,0 +1,864 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import errno +import socket +import sys +import time +import urllib.parse +from http.client import BadStatusLine, HTTPConnection, NotConnected + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import HTTPSConnection, ntob, tonative +from cherrypy.test import helper + + +timeout = 1 +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + + +def setup_server(): + + def raise500(): + raise cherrypy.HTTPError(500) + + class Root: + + @cherrypy.expose + def index(self): + return pov + page1 = index + page2 = index + page3 = index + + @cherrypy.expose + def hello(self): + return 'Hello, world!' + + @cherrypy.expose + def timeout(self, t): + return str(cherrypy.server.httpserver.timeout) + + @cherrypy.expose + @cherrypy.config(**{'response.stream': True}) + def stream(self, set_cl=False): + if set_cl: + cherrypy.response.headers['Content-Length'] = 10 + + def content(): + for x in range(10): + yield str(x) + + return content() + + @cherrypy.expose + def error(self, code=500): + raise cherrypy.HTTPError(code) + + @cherrypy.expose + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % cherrypy.request.body.read() + + @cherrypy.expose + def custom(self, response_code): + cherrypy.response.status = response_code + return 'Code = %s' % response_code + + @cherrypy.expose + @cherrypy.config(**{'hooks.on_start_resource': raise500}) + def err_before_read(self): + return 'ok' + + @cherrypy.expose + def one_megabyte_of_a(self): + return ['a' * 1024] * 1024 + + @cherrypy.expose + # Turn off the encoding tool so it doens't collapse + # our response body and reclaculate the Content-Length. + @cherrypy.config(**{'tools.encode.on': False}) + def custom_cl(self, body, cl): + cherrypy.response.headers['Content-Length'] = cl + if not isinstance(body, list): + body = [body] + newbody = [] + for chunk in body: + if isinstance(chunk, str): + chunk = chunk.encode('ISO-8859-1') + newbody.append(chunk) + return newbody + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': timeout, + }) + + +class ConnectionCloseTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make another request on the same connection. + self.getPage('/page1') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Test client-side close. + self.getPage('/page2', headers=[('Connection', 'close')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, '/') + + def test_Streaming_no_len(self): + try: + self._streaming(set_cl=False) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def test_Streaming_with_len(self): + try: + self._streaming(set_cl=True) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def _streaming(self, set_cl): + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + self.getPage('/stream?set_cl=Yes') + self.assertHeader('Content-Length') + self.assertNoHeader('Connection', 'close') + self.assertNoHeader('Transfer-Encoding') + + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + self.getPage('/stream') + self.assertNoHeader('Content-Length') + self.assertStatus('200 OK') + self.assertBody('0123456789') + + chunked_response = False + for k, v in self.headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + self.assertNoHeader('Connection', 'close') + else: + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should + # error. + self.assertRaises(NotConnected, self.getPage, '/') + + # Try HEAD. See + # https://github.com/cherrypy/cherrypy/issues/864. + self.getPage('/stream', method='HEAD') + self.assertStatus('200 OK') + self.assertBody('') + self.assertNoHeader('Transfer-Encoding') + else: + self.PROTOCOL = 'HTTP/1.0' + + self.persistent = True + + # Make the first request and assert Keep-Alive. + self.getPage('/', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'Keep-Alive') + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + self.getPage('/stream?set_cl=Yes', + headers=[('Connection', 'Keep-Alive')]) + self.assertHeader('Content-Length') + self.assertHeader('Connection', 'Keep-Alive') + self.assertNoHeader('Transfer-Encoding') + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When a Content-Length is not provided, + # the server should close the connection. + self.getPage('/stream', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody('0123456789') + + self.assertNoHeader('Content-Length') + self.assertNoHeader('Connection', 'Keep-Alive') + self.assertNoHeader('Transfer-Encoding') + + # Make another request on the same connection, which should + # error. + self.assertRaises(NotConnected, self.getPage, '/') + + def test_HTTP10_KeepAlive(self): + self.PROTOCOL = 'HTTP/1.0' + if self.scheme == 'https': + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a normal HTTP/1.0 request. + self.getPage('/page2') + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 + # self.assertNoHeader("Connection") + + # Test a keep-alive HTTP/1.0 request. + self.persistent = True + + self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'Keep-Alive') + + # Remove the keep-alive header again. + self.getPage('/page3') + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 + # self.assertNoHeader("Connection") + + +class PipelineTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11_Timeout(self): + # If we timeout without sending any data, + # the server will close the conn with a 408. + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Connect but send nothing. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + # Connect but send half the headers only. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % self.HOST).encode('ascii')) + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The conn should have already sent 408. + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + def test_HTTP11_Timeout_after_request(self): + # If we timeout after at least one request has succeeded, + # the server will close the conn without 408. + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(str(timeout)) + + # Make a second request on the same socket + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody('Hello, world!') + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + msg = ( + "Writing to timed out socket didn't fail as it should have: %s") + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail(msg % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail(msg % response.read()) + + conn.close() + + # Make another request on a new socket, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + + # Make another request on the same socket, + # but timeout on the headers + conn.send(b'GET /hello HTTP/1.1') + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail(msg % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail(msg % response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + conn.close() + + def test_HTTP11_pipelining(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Test pipelining. httplib doesn't support this directly. + self.persistent = True + conn = self.HTTP_CONN + + # Put request 1 + conn.putrequest('GET', '/hello', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method='GET') + # there is a bug in python3 regarding the buffering of + # ``conn.sock``. Until that bug get's fixed we will + # monkey patch the ``response`` instance. + # https://bugs.python.org/issue23377 + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + conn.close() + + def test_100_Continue(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + conn = self.HTTP_CONN + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + try: + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conn.send(ntob("d'oh")) + response = conn.response_class(conn.sock, method='POST') + version, status, reason = response._read_status() + self.assertNotEqual(status, 100) + finally: + conn.close() + + # Now try a page with an Expect header... + try: + conn.connect() + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '17') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + line = response.fp.readline().strip() + if line: + self.fail( + '100 Continue should not output any headers. Got %r' % + line) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + finally: + conn.close() + + +class ConnectionTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_readall_or_close(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + if self.scheme == 'https': + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a max of 0 (the default) and then reset to what it was above. + old_max = cherrypy.server.max_request_body_size + for new_max in (0, old_max): + cherrypy.server.max_request_body_size = new_max + + self.persistent = True + conn = self.HTTP_CONN + + # Get a POST page with an error + conn.putrequest('POST', '/err_before_read', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '1000') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + conn.send(ntob('x' * 1000)) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + + # Now try a working page with an Expect header... + conn._output(b'POST /upload HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._output(b'Content-Type: text/plain') + conn._output(b'Content-Length: 17') + conn._output(b'Expect: 100-continue') + conn._send_output() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + def test_No_Message_Body(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make a 204 request on the same connection. + self.getPage('/custom/204') + self.assertStatus(204) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + # Make a 304 request on the same connection. + self.getPage('/custom/304') + self.assertStatus(304) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + def test_Chunked_Encoding(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + if (hasattr(self, 'harness') and + 'modpython' in self.harness.__class__.__name__.lower()): + # mod_python forbids chunked encoding + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + conn = self.HTTP_CONN + + # Try a normal chunked request (with extensions) + body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' + 'Content-Type: application/json\r\n' + '\r\n') + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Trailer', 'Content-Type') + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader('Content-Length', '3') + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus('200 OK') + self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy') + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n') + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Content-Type', 'text/plain') + # Chunked requests don't need a content-length + # # conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + conn.close() + + def test_Content_Length_in(self): + # Try a non-chunked request where Content-Length exceeds + # server.max_request_body_size. Assert error before body send. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '9999') + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + self.assertBody('The entity sent with the request exceeds ' + 'the maximum allowed bytes.') + conn.close() + + def test_Content_Length_out_preheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5', + skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + self.assertBody( + 'The requested resource returned more bytes than the ' + 'declared Content-Length.') + conn.close() + + def test_Content_Length_out_postheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest( + 'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5', + skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody('I too') + conn.close() + + def test_598(self): + tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' + url = tmpl.format( + scheme=self.scheme, + host=self.HOST, + port=self.PORT, + ) + remote_data_conn = urllib.request.urlopen(url) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + else: + buf += data + remaining -= len(data) + + self.assertEqual(len(buf), 1024 * 1024) + self.assertEqual(buf, ntob('a' * 1024 * 1024)) + self.assertEqual(remaining, 0) + remote_data_conn.close() + + +def setup_upload_server(): + + class Root: + @cherrypy.expose + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % tonative(cherrypy.request.body.read()) + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': 10, + 'server.accepted_queue_size': 5, + 'server.accepted_queue_timeout': 0.1, + }) + + +reset_names = 'ECONNRESET', 'WSAECONNRESET' +socket_reset_errors = [ + getattr(errno, name) + for name in reset_names + if hasattr(errno, name) +] +'reset error numbers available on this platform' + +socket_reset_errors += [ + # Python 3.5 raises an http.client.RemoteDisconnected + # with this message + 'Remote end closed connection without response', +] + + +class LimitedRequestQueueTests(helper.CPWebCase): + setup_server = staticmethod(setup_upload_server) + + def test_queue_full(self): + conns = [] + overflow_conn = None + + try: + # Make 15 initial requests and leave them open, which should use + # all of wsgiserver's WorkerThreads and fill its Queue. + for i in range(15): + conn = self.HTTP_CONN(self.HOST, self.PORT) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conns.append(conn) + + # Now try a 16th conn, which should be closed by the + # server immediately. + overflow_conn = self.HTTP_CONN(self.HOST, self.PORT) + # Manually connect since httplib won't let us set a timeout + for res in socket.getaddrinfo(self.HOST, self.PORT, 0, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + overflow_conn.sock = socket.socket(af, socktype, proto) + overflow_conn.sock.settimeout(5) + overflow_conn.sock.connect(sa) + break + + overflow_conn.putrequest('GET', '/', skip_host=True) + overflow_conn.putheader('Host', self.HOST) + overflow_conn.endheaders() + response = overflow_conn.response_class( + overflow_conn.sock, + method='GET', + ) + try: + response.begin() + except socket.error as exc: + if exc.args[0] in socket_reset_errors: + pass # Expected. + else: + tmpl = ( + 'Overflow conn did not get RST. ' + 'Got {exc.args!r} instead' + ) + raise AssertionError(tmpl.format(**locals())) + except BadStatusLine: + # This is a special case in OS X. Linux and Windows will + # RST correctly. + assert sys.platform == 'darwin' + else: + raise AssertionError('Overflow conn did not get RST ') + finally: + for conn in conns: + conn.send(b'done') + response = conn.response_class(conn.sock, method='POST') + response.begin() + self.body = response.read() + self.assertBody("thanks for 'done'") + self.assertEqual(response.status, 200) + conn.close() + if overflow_conn: + overflow_conn.close() + + +class BadRequestTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_No_CRLF(self): + self.persistent = True + + conn = self.HTTP_CONN + conn.send(b'GET /hello HTTP/1.1\n\n') + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.body = response.read() + self.assertBody('HTTP requires CRLF terminators') + conn.close() + + conn.connect() + conn.send(b'GET /hello HTTP/1.1\r\n\n') + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.body = response.read() + self.assertBody('HTTP requires CRLF terminators') + conn.close() diff --git a/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_core.py b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_core.py new file mode 100644 index 000000000..42460b3f4 --- /dev/null +++ b/awesome_venv/lib/python3.10/site-packages/cherrypy/test/test_core.py @@ -0,0 +1,825 @@ +# coding: utf-8 + +"""Basic tests for the CherryPy core: request handling.""" + +import os +import sys +import types + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cptools, tools +from cherrypy.lib import httputil, static + +from cherrypy.test._test_decorators import ExposeExamples +from cherrypy.test import helper + + +localDir = os.path.dirname(__file__) +favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') + +# Client-side code # + + +class CoreRequestHandlingTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'hello' + + favicon_ico = tools.staticfile.handler(filename=favicon_path) + + @cherrypy.expose + def defct(self, newct): + newct = 'text/%s' % newct + cherrypy.config.update({'tools.response_headers.on': True, + 'tools.response_headers.headers': + [('Content-Type', newct)]}) + + @cherrypy.expose + def baseurl(self, path_info, relative=None): + return cherrypy.url(path_info, relative=bool(relative)) + + root = Root() + root.expose_dec = ExposeExamples() + + class TestType(type): + + """Metaclass which automatically exposes all functions in each + subclass, and adds an instance of the subclass as an attribute + of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in dct.values(): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object, ), {}) + + @cherrypy.config(**{'tools.trailing_slash.on': False}) + class URL(Test): + + def index(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def leaf(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def qs(self, qs): + return cherrypy.url(qs=qs) + + def log_status(): + Status.statuses.append(cherrypy.response.status) + cherrypy.tools.log_status = cherrypy.Tool( + 'on_end_resource', log_status) + + class Status(Test): + + def index(self): + return 'normal' + + def blank(self): + cherrypy.response.status = '' + + # According to RFC 2616, new status codes are OK as long as they + # are between 100 and 599. + + # Here is an illegal code... + def illegal(self): + cherrypy.response.status = 781 + return 'oops' + + # ...and here is an unknown but legal code. + def unknown(self): + cherrypy.response.status = '431 My custom error' + return 'funky' + + # Non-numeric code + def bad(self): + cherrypy.response.status = 'error' + return 'bad news' + + statuses = [] + + @cherrypy.config(**{'tools.log_status.on': True}) + def on_end_resource_stage(self): + return repr(self.statuses) + + class Redirect(Test): + + @cherrypy.config(**{ + 'tools.err_redirect.on': True, + 'tools.err_redirect.url': '/errpage', + 'tools.err_redirect.internal': False, + }) + class Error: + @cherrypy.expose + def index(self): + raise NameError('redirect_test') + + error = Error() + + def index(self): + return 'child' + + def custom(self, url, code): + raise cherrypy.HTTPRedirect(url, code) + + @cherrypy.config(**{'tools.trailing_slash.extra': True}) + def by_code(self, code): + raise cherrypy.HTTPRedirect('somewhere%20else', code) + + def nomodify(self): + raise cherrypy.HTTPRedirect('', 304) + + def proxy(self): + raise cherrypy.HTTPRedirect('proxy', 305) + + def stringify(self): + return str(cherrypy.HTTPRedirect('/')) + + def fragment(self, frag): + raise cherrypy.HTTPRedirect('/some/url#%s' % frag) + + def url_with_quote(self): + raise cherrypy.HTTPRedirect("/some\"url/that'we/want") + + def url_with_xss(self): + raise cherrypy.HTTPRedirect( + "/someurl/that'we/want") + + def url_with_unicode(self): + raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) + + def login_redir(): + if not getattr(cherrypy.request, 'login', None): + raise cherrypy.InternalRedirect('/internalredirect/login') + tools.login_redir = _cptools.Tool('before_handler', login_redir) + + def redir_custom(): + raise cherrypy.InternalRedirect('/internalredirect/custom_err') + + class InternalRedirect(Test): + + def index(self): + raise cherrypy.InternalRedirect('/') + + @cherrypy.expose + @cherrypy.config(**{'hooks.before_error_response': redir_custom}) + def choke(self): + return 3 / 0 + + def relative(self, a, b): + raise cherrypy.InternalRedirect('cousin?t=6') + + def cousin(self, t): + assert cherrypy.request.prev.closed + return cherrypy.request.prev.query_string + + def petshop(self, user_id): + if user_id == 'parrot': + # Trade it for a slug when redirecting + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=slug') + elif user_id == 'terrier': + # Trade it for a fish when redirecting + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=fish') + else: + # This should pass the user_id through to getImagesByUser + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=%s' % str(user_id)) + + # We support Python 2.3, but the @-deco syntax would look like + # this: + # @tools.login_redir() + def secure(self): + return 'Welcome!' + secure = tools.login_redir()(secure) + # Since calling the tool returns the same function you pass in, + # you could skip binding the return value, and just write: + # tools.login_redir()(secure) + + def login(self): + return 'Please log in' + + def custom_err(self): + return 'Something went horribly wrong.' + + @cherrypy.config(**{'hooks.before_request_body': redir_custom}) + def early_ir(self, arg): + return 'whatever' + + class Image(Test): + + def getImagesByUser(self, user_id): + return '0 images for %s' % user_id + + class Flatten(Test): + + def as_string(self): + return 'content' + + def as_list(self): + return ['con', 'tent'] + + def as_yield(self): + yield b'content' + + @cherrypy.config(**{'tools.flatten.on': True}) + def as_dblyield(self): + yield self.as_yield() + + def as_refyield(self): + for chunk in self.as_yield(): + yield chunk + + class Ranges(Test): + + def get_ranges(self, bytes): + return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) + + def slice_file(self): + path = os.path.join(os.getcwd(), os.path.dirname(__file__)) + return static.serve_file( + os.path.join(path, 'static/index.html')) + + class Cookies(Test): + + def single(self, name): + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def multiple(self, names): + list(map(self.single, names)) + + def append_headers(header_list, debug=False): + if debug: + cherrypy.log( + 'Extending response headers with %s' % repr(header_list), + 'TOOLS.APPEND_HEADERS') + cherrypy.serving.response.header_list.extend(header_list) + cherrypy.tools.append_headers = cherrypy.Tool( + 'on_end_resource', append_headers) + + class MultiHeader(Test): + + def header_list(self): + pass + header_list = cherrypy.tools.append_headers(header_list=[ + (b'WWW-Authenticate', b'Negotiate'), + (b'WWW-Authenticate', b'Basic realm="foo"'), + ])(header_list) + + def commas(self): + cherrypy.response.headers[ + 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + + cherrypy.tree.mount(root) + + def testStatus(self): + self.getPage('/status/') + self.assertBody('normal') + self.assertStatus(200) + + self.getPage('/status/blank') + self.assertBody('') + self.assertStatus(200) + + self.getPage('/status/illegal') + self.assertStatus(500) + msg = 'Illegal response status from server (781 is out of range).' + self.assertErrorPage(500, msg) + + if not getattr(cherrypy.server, 'using_apache', False): + self.getPage('/status/unknown') + self.assertBody('funky') + self.assertStatus(431) + + self.getPage('/status/bad') + self.assertStatus(500) + msg = "Illegal response status from server ('error' is non-numeric)." + self.assertErrorPage(500, msg) + + def test_on_end_resource_status(self): + self.getPage('/status/on_end_resource_stage') + self.assertBody('[]') + self.getPage('/status/on_end_resource_stage') + self.assertBody(repr(['200 OK'])) + + def testSlashes(self): + # Test that requests for index methods without a trailing slash + # get redirected to the same URI path with a trailing slash. + # Make sure GET params are preserved. + self.getPage('/redirect?id=3') + self.assertStatus(301) + self.assertMatchesBody( + '' + '%s/redirect/[?]id=3' % (self.base(), self.base()) + ) + + if self.prefix(): + # Corner case: the "trailing slash" redirect could be tricky if + # we're using a virtual root and the URI is "/vroot" (no slash). + self.getPage('') + self.assertStatus(301) + self.assertMatchesBody("%s/" % + (self.base(), self.base())) + + # Test that requests for NON-index methods WITH a trailing slash + # get redirected to the same URI path WITHOUT a trailing slash. + # Make sure GET params are preserved. + self.getPage('/redirect/by_code/?code=307') + self.assertStatus(301) + self.assertMatchesBody( + "" + '%s/redirect/by_code[?]code=307' + % (self.base(), self.base()) + ) + + # If the trailing_slash tool is off, CP should just continue + # as if the slashes were correct. But it needs some help + # inside cherrypy.url to form correct output. + self.getPage('/url?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + + def testRedirect(self): + self.getPage('/redirect/') + self.assertBody('child') + self.assertStatus(200) + + self.getPage('/redirect/by_code?code=300') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(300) + + self.getPage('/redirect/by_code?code=301') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(301) + + self.getPage('/redirect/by_code?code=302') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(302) + + self.getPage('/redirect/by_code?code=303') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(303) + + self.getPage('/redirect/by_code?code=307') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(307) + + self.getPage('/redirect/by_code?code=308') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(308) + + self.getPage('/redirect/nomodify') + self.assertBody('') + self.assertStatus(304) + + self.getPage('/redirect/proxy') + self.assertBody('') + self.assertStatus(305) + + # HTTPRedirect on error + self.getPage('/redirect/error/') + self.assertStatus(('302 Found', '303 See Other')) + self.assertInBody('/errpage') + + # Make sure str(HTTPRedirect()) works. + self.getPage('/redirect/stringify', protocol='HTTP/1.0') + self.assertStatus(200) + self.assertBody("(['%s/'], 302)" % self.base()) + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.getPage('/redirect/stringify', protocol='HTTP/1.1') + self.assertStatus(200) + self.assertBody("(['%s/'], 303)" % self.base()) + + # check that #fragments are handled properly + # http://skrb.org/ietf/http_errata.html#location-fragments + frag = 'foo' + self.getPage('/redirect/fragment/%s' % frag) + self.assertMatchesBody( + r"\2\/some\/url\#%s" % ( + frag, frag)) + loc = self.assertHeader('Location') + assert loc.endswith('#%s' % frag) + self.assertStatus(('302 Found', '303 See Other')) + + # check injection protection + # See https://github.com/cherrypy/cherrypy/issues/1003 + self.getPage( + '/redirect/custom?' + 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') + self.assertStatus(303) + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') + + def assertValidXHTML(): + from xml.etree import ElementTree + try: + ElementTree.fromstring( + '%s' % self.body, + ) + except ElementTree.ParseError: + self._handlewebError( + 'automatically generated redirect did not ' + 'generate well-formed html', + ) + + # check redirects to URLs generated valid HTML - we check this + # by seeing if it appears as valid XHTML. + self.getPage('/redirect/by_code?code=303') + self.assertStatus(303) + assertValidXHTML() + + # do the same with a url containing quote characters. + self.getPage('/redirect/url_with_quote') + self.assertStatus(303) + assertValidXHTML() + + def test_redirect_with_xss(self): + """A redirect to a URL with HTML injected should result + in page contents escaped.""" + self.getPage('/redirect/url_with_xss') + self.assertStatus(303) + assert b'